diff --git a/RulesEngine.sln b/RulesEngine.sln index ba233bd3..41446be0 100644 --- a/RulesEngine.sln +++ b/RulesEngine.sln @@ -17,6 +17,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig CHANGELOG.md = CHANGELOG.md global.json = global.json + LICENSE = LICENSE README.md = README.md schema\workflow-list-schema.json = schema\workflow-list-schema.json schema\workflow-schema.json = schema\workflow-schema.json diff --git a/src/RulesEngine/Actions/ActionContext.cs b/src/RulesEngine/Actions/ActionContext.cs index a7c3cbb5..2ac01d1d 100644 --- a/src/RulesEngine/Actions/ActionContext.cs +++ b/src/RulesEngine/Actions/ActionContext.cs @@ -1,11 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Text.Json; using RulesEngine.Models; using System; using System.Collections.Generic; +using System.Text.Json; using System.Threading; +using static FastExpressionCompiler.ImTools.SmallMap; namespace RulesEngine.Actions { @@ -25,25 +26,16 @@ public ActionContext(IDictionary context, RuleResultTree parentR _context = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var kv in context) { - string key = kv.Key; - string value; - switch (kv.Value.GetType().Name) - { - case "String": - case "JsonElement": - value = kv.Value.ToString(); - break; - default: - value = JsonSerializer.Serialize(kv.Value); - break; - - } - _context.Add(key, value); + if (kv.Value == null) + continue; + else if (kv.Value is string || kv.Value is JsonElement) + _context.Add(kv.Key, kv.Value.ToString()); + else + _context.Add(kv.Key, JsonSerializer.Serialize(kv.Value)); } _parentResult = parentResult; } - public RuleResultTree GetParentRuleResult() { return _parentResult; @@ -68,9 +60,8 @@ public T GetContext(string name) try { if (typeof(T) == typeof(string)) - { - return (T)Convert.ChangeType(_context[name], typeof(T)); - } + return (T)(object)_context[name]; + return JsonSerializer.Deserialize(_context[name]); } catch (KeyNotFoundException) diff --git a/src/RulesEngine/CustomTypeProvider.cs b/src/RulesEngine/CustomTypeProvider.cs index 4171ec7f..b9284b5b 100644 --- a/src/RulesEngine/CustomTypeProvider.cs +++ b/src/RulesEngine/CustomTypeProvider.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using RulesEngine.HelperFunctions; @@ -9,9 +9,17 @@ namespace RulesEngine { + /// + /// Provides custom types to System.Linq.Dynamic.Core for use in dynamic rule expressions. + /// public class CustomTypeProvider : DefaultDynamicLinqCustomTypeProvider { private HashSet _types; + + /// + /// Initializes a new instance of the class. + /// + /// An array of custom types to make available in dynamic expressions. public CustomTypeProvider(Type[] types) : base(ParsingConfig.Default) { _types = new HashSet(types ?? new Type[] { }) { @@ -20,6 +28,10 @@ public CustomTypeProvider(Type[] types) : base(ParsingConfig.Default) }; } + /// + /// Gets the set of custom types available in dynamic expressions. + /// + /// A hash set of custom types. public override HashSet GetCustomTypes() { return _types; diff --git a/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs b/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs index aea7e295..96d51a09 100644 --- a/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs +++ b/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs @@ -16,20 +16,13 @@ namespace RulesEngine.ExpressionBuilders public class RuleExpressionParser { private readonly ReSettings _reSettings; - private readonly IDictionary _methodInfo; + private static readonly MethodInfo _dictAddMethod = typeof(Dictionary).GetMethod("Add", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(string), typeof(object) }, null); public RuleExpressionParser(ReSettings reSettings = null) { _reSettings = reSettings ?? new ReSettings(); - _methodInfo = new Dictionary(); - PopulateMethodInfo(); - } - - private void PopulateMethodInfo() - { - var dict_add = typeof(Dictionary).GetMethod("Add", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(string), typeof(object) }, null); - _methodInfo.Add("dict_add", dict_add); } + public Expression Parse(string expression, ParameterExpression[] parameters, Type returnType) { if (parameters == null) @@ -142,8 +135,7 @@ private Expression>> CreateDictionaryExp body.AddRange(variableExpressions); var dict = Expression.Variable(typeof(Dictionary)); - var add = _methodInfo["dict_add"]; - + body.Add(Expression.Assign(dict, Expression.New(typeof(Dictionary)))); variableExp.Add(dict); @@ -156,7 +148,7 @@ private Expression>> CreateDictionaryExp var key = Expression.Constant(ruleExpParams[i].ParameterExpression.Name); var value = Expression.Convert(ruleExpParams[i].ParameterExpression, typeof(object)); variableExp.Add(ruleExpParams[i].ParameterExpression); - body.Add(Expression.Call(dict, add, key, value)); + body.Add(Expression.Call(dict, _dictAddMethod, key, value)); } // Return value diff --git a/src/RulesEngine/HelperFunctions/ErrorMessageFormatter.cs b/src/RulesEngine/HelperFunctions/ErrorMessageFormatter.cs new file mode 100644 index 00000000..add3b4d0 --- /dev/null +++ b/src/RulesEngine/HelperFunctions/ErrorMessageFormatter.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace RulesEngine.HelperFunctions +{ + /// + /// Formats error messages for rule results by replacing parameter placeholders with actual values. + /// + internal class ErrorMessageFormatter + { + /// + /// Regex pattern to match parameter placeholders in the format $(ParameterName). + /// + private const string ParamParseRegex = @"\$\(([^)]+)\)"; + + private readonly ReSettings _reSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The rules engine settings. + /// Thrown when is null. + public ErrorMessageFormatter(ReSettings reSettings) + { + _reSettings = reSettings ?? throw new ArgumentNullException(nameof(reSettings)); + } + + /// + /// Formats error messages for the specified rule results by replacing parameter placeholders with actual values. + /// + /// The collection of rule results to format. + public void FormatErrorMessages(IEnumerable ruleResultList) + { + if (!_reSettings.EnableFormattedErrorMessage) + { + return; + } + + foreach (var ruleResult in ruleResultList?.Where(r => !r.IsSuccess)) + { + var errorMessage = ruleResult?.Rule?.ErrorMessage; + if (string.IsNullOrWhiteSpace(ruleResult.ExceptionMessage) && errorMessage != null) + { + ruleResult.ExceptionMessage = BuildErrorMessage(errorMessage, ruleResult.Inputs); + } + } + } + + /// + /// Builds an error message by replacing parameter placeholders with values from the inputs. + /// + /// The error message template containing placeholders. + /// The input values to substitute into the placeholders. + /// The formatted error message with placeholders replaced by actual values. + private static string BuildErrorMessage(string errorMessage, IDictionary inputs) + { + var errorParameters = Regex.Matches(errorMessage, ParamParseRegex); + + foreach (var param in errorParameters) + { + var paramVal = param?.ToString(); + var property = paramVal?.Substring(2, paramVal.Length - 3); + + if (string.IsNullOrEmpty(property)) + { + continue; + } + + if (property.Split('.').Length > 1) + { + var parts = property.Split(new[] { '.' }, 2); + var typeName = parts[0]; + var propertyName = parts[1]; + errorMessage = UpdateErrorMessage(errorMessage, inputs, property, typeName, propertyName); + } + else + { + var model = inputs?.FirstOrDefault(c => string.Equals(c.Key, property)).Value; + var value = model != null ? JsonSerializer.Serialize(model) : null; + errorMessage = errorMessage.Replace($"$({property})", value ?? $"$({property})"); + } + } + + return errorMessage; + } + + /// + /// Updates an error message by replacing a specific parameter placeholder with a JSON property value. + /// + /// The error message template. + /// The input values dictionary. + /// The full property placeholder (e.g., "TypeName.PropertyName"). + /// The type name to look up in inputs. + /// The JSON property name to extract. + /// The updated error message with the placeholder replaced. + private static string UpdateErrorMessage(string errorMessage, IDictionary inputs, string property, string typeName, string propertyName) + { + var model = inputs?.FirstOrDefault(c => string.Equals(c.Key, typeName)).Value; + + if (model != null) + { + using (var jDoc = JsonSerializer.SerializeToDocument(model)) + { + errorMessage = jDoc.RootElement.TryGetProperty(propertyName, out var jElement) ? + errorMessage.Replace($"$({property})", jElement.GetRawText() ?? $"({property})") : + errorMessage.Replace($"$({property})", $"({property})"); + } + } + + return errorMessage; + } + } +} diff --git a/src/RulesEngine/HelperFunctions/MemCache.cs b/src/RulesEngine/HelperFunctions/MemCache.cs index 938dbaeb..43bba598 100644 --- a/src/RulesEngine/HelperFunctions/MemCache.cs +++ b/src/RulesEngine/HelperFunctions/MemCache.cs @@ -78,20 +78,31 @@ public T Set(string key, T value, DateTimeOffset? expiry = null) { var fixedExpiry = expiry ?? DateTimeOffset.MaxValue; - // If at capacity, evict oldest by expiry - while (_cacheDictionary.Count > _config.SizeLimit) + // If over capacity, scan once and remove expired or arbitrary + if (_cacheDictionary.Count >= _config.SizeLimit) { - var oldest = _cacheDictionary.OrderBy(kv => kv.Value.expiry).FirstOrDefault(); - if (oldest.Key != null) + foreach (var kv in _cacheDictionary) { - _cacheDictionary.TryRemove(oldest.Key, out _); - } - else - { - break; // Shouldn't happen but prevents infinite loop + if (_cacheDictionary.Count < _config.SizeLimit) + break; + + if (kv.Value.expiry < DateTimeOffset.UtcNow) + { + _cacheDictionary.TryRemove(kv.Key, out _); + } } } + // If still at capacity, remove arbitrary entries + while (_cacheDictionary.Count > _config.SizeLimit) + { + var keyToRemove = _cacheDictionary.Keys.FirstOrDefault(); + if (keyToRemove == null) + break; + + _cacheDictionary.TryRemove(keyToRemove, out _); + } + _cacheDictionary.AddOrUpdate(key, (value, fixedExpiry), (k, v) => (value, fixedExpiry)); return value; } diff --git a/src/RulesEngine/HelperFunctions/Utils.cs b/src/RulesEngine/HelperFunctions/Utils.cs index 940cf14e..a034270a 100644 --- a/src/RulesEngine/HelperFunctions/Utils.cs +++ b/src/RulesEngine/HelperFunctions/Utils.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using RulesEngine.Models; using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Dynamic; using System.Linq; @@ -10,12 +12,12 @@ using System.Reflection; using System.Text.Json; -using RulesEngine.Models; - namespace RulesEngine.HelperFunctions { public static class Utils { + private static readonly ConcurrentDictionary _typeCache = new(); + public static object GetTypedObject(dynamic input) { if (input is ExpandoObject) @@ -28,50 +30,51 @@ public static object GetTypedObject(dynamic input) return input; } } + public static Type CreateAbstractClassType(dynamic input) { List props = new List(); - if (input is JsonElement jsonElement) - { - input = jsonElement.ToExpandoObject(); - } - if (input == null) - { - return typeof(object); - } - if (!(input is ExpandoObject)) + try { - return input.GetType(); - } + if (input is JsonElement jsonElement) + input = jsonElement.ToExpandoObject(); + + if (input == null) + return typeof(object); + + if (!(input is ExpandoObject)) + return input.GetType(); - else - { foreach (var expando in (IDictionary)input) { - Type value; + Type t; if (expando.Value is IList list) { if (list.Count == 0) { - value = typeof(List>); + t = typeof(List>); } else { var internalType = CreateAbstractClassType(list[0]); - value = typeof(List<>).MakeGenericType(internalType); + t = typeof(List<>).MakeGenericType(internalType); } } else { - value = CreateAbstractClassType(expando.Value); + t = CreateAbstractClassType(expando.Value); } - props.Add(new DynamicProperty(expando.Key, value)); + props.Add(new DynamicProperty(expando.Key, t)); } } + catch (Exception ex) + { + throw new Exception($"Error creating abstract class type: {ex.Message}", ex); + } - var type = DynamicClassFactory.CreateType(props); - return type; + var cacheKey = GetTypeCacheKey(props); + return _typeCache.GetOrAdd(cacheKey, _ => DynamicClassFactory.CreateType(props)); } public static object CreateObject(Type type, dynamic input) @@ -85,14 +88,14 @@ public static object CreateObject(Type type, dynamic input) { return Convert.ChangeType(input, type); } + object obj = Activator.CreateInstance(type); var typeProps = type.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance).ToDictionary(c => c.Name); foreach (var expando in (IDictionary)input) { - if (typeProps.ContainsKey(expando.Key) && - expando.Value != null && (expando.Value.GetType().Name != "DBNull" || expando.Value != DBNull.Value)) + if (typeProps.ContainsKey(expando.Key) && expando.Value != null && !(expando.Value is DBNull)) { object val; var propInfo = typeProps[expando.Key]; @@ -105,7 +108,7 @@ public static object CreateObject(Type type, dynamic input) { var internalType = propInfo.PropertyType.GenericTypeArguments.FirstOrDefault() ?? typeof(object); var temp = (IList)expando.Value; - var newList = new List().Cast(internalType).ToList(internalType); + var newList = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(internalType)); for (int i = 0; i < temp.Count; i++) { var child = CreateObject(internalType, temp[i]); @@ -128,18 +131,6 @@ public static object CreateObject(Type type, dynamic input) return obj; } - private static IEnumerable Cast(this IEnumerable self, Type innerType) - { - var methodInfo = typeof(Enumerable).GetMethod("Cast"); - var genericMethod = methodInfo.MakeGenericMethod(innerType); - return genericMethod.Invoke(null, new[] { self }) as IEnumerable; - } - - private static IList ToList(this IEnumerable self, Type innerType) - { - var methodInfo = typeof(Enumerable).GetMethod("ToList"); - var genericMethod = methodInfo.MakeGenericMethod(innerType); - return genericMethod.Invoke(null, new[] { self }) as IList; - } + private static string GetTypeCacheKey(List props) => string.Join("|", props.Select((p, i) => $"{i}:{p.Name}:{p.Type.FullName}")); } } \ No newline at end of file diff --git a/src/RulesEngine/Models/ScopedParam.cs b/src/RulesEngine/Models/ScopedParam.cs index 2ccd61bd..91608aec 100644 --- a/src/RulesEngine/Models/ScopedParam.cs +++ b/src/RulesEngine/Models/ScopedParam.cs @@ -5,7 +5,8 @@ namespace RulesEngine.Models { - /// Class LocalParam. + /// + /// Class LocalParam. /// [ExcludeFromCodeCoverage] public class ScopedParam @@ -16,7 +17,7 @@ public class ScopedParam /// /// /// The name of the rule. - /// ] + /// public string Name { get; set; } /// diff --git a/src/RulesEngine/RuleCompiler.cs b/src/RulesEngine/RuleCompiler.cs index ca6f836f..78d48351 100644 --- a/src/RulesEngine/RuleCompiler.cs +++ b/src/RulesEngine/RuleCompiler.cs @@ -14,17 +14,17 @@ namespace RulesEngine { /// - /// Rule compilers + /// Compiles rule definitions into executable delegates, handling nested rule operators and scoped parameters. /// internal class RuleCompiler { /// - /// The nested operators + /// The nested operators supported for grouping child rules. /// private readonly ExpressionType[] nestedOperators = new ExpressionType[] { ExpressionType.And, ExpressionType.AndAlso, ExpressionType.Or, ExpressionType.OrElse }; /// - /// The expression builder factory + /// The factory used to get the appropriate expression builder for a rule expression type. /// private readonly RuleExpressionBuilderFactory _expressionBuilderFactory; private readonly ReSettings _reSettings; @@ -41,13 +41,14 @@ internal RuleCompiler(RuleExpressionBuilderFactory expressionBuilderFactory, ReS } /// - /// Compiles the rule + /// Compiles a rule into an executable delegate that evaluates the rule against input parameters. /// - /// - /// - /// - /// - /// Compiled func delegate + /// The rule to compile. + /// The type of rule expression. + /// The input parameters available to the rule. + /// Lazy-loaded global expression parameters. + /// A compiled delegate that evaluates the rule. + /// Thrown when is null. internal RuleFunc CompileRule(Rule rule, RuleExpressionType ruleExpressionType, RuleParameter[] ruleParams, Lazy globalParams) { if (rule == null) @@ -73,12 +74,11 @@ internal RuleFunc CompileRule(Rule rule, RuleExpressionType rule } /// - /// Gets the expression for rule. + /// Gets the delegate for evaluating a rule, including its local scoped parameters. /// - /// The rule. - /// The type parameter expressions. - /// The rule input exp. - /// + /// The rule to evaluate. + /// The input parameters available to the rule. + /// A delegate that evaluates the rule. private RuleFunc GetDelegateForRule(Rule rule, RuleParameter[] ruleParams) { var scopedParamList = GetRuleExpressionParameters(rule.RuleExpressionType, rule?.LocalParams, ruleParams); @@ -101,7 +101,14 @@ private RuleFunc GetDelegateForRule(Rule rule, RuleParameter[] r return GetWrappedRuleFunc(rule, ruleFn, ruleParams, scopedParamList); } - internal RuleExpressionParameter[] GetRuleExpressionParameters(RuleExpressionType ruleExpressionType,IEnumerable localParams, RuleParameter[] ruleParams) + /// + /// Gets expression parameters for the specified local scoped parameters. + /// + /// The type of rule expression. + /// The local scoped parameters defined on the rule. + /// The input parameters available to the rule. + /// An array of rule expression parameters. + internal RuleExpressionParameter[] GetRuleExpressionParameters(RuleExpressionType ruleExpressionType, IEnumerable localParams, RuleParameter[] ruleParams) { if(!_reSettings.EnableScopedParams) { @@ -143,13 +150,12 @@ internal RuleExpressionParameter[] GetRuleExpressionParameters(RuleExpressionTyp } /// - /// Builds the expression. + /// Builds a rule function for a simple (non-nested) rule. /// - /// The rule. - /// The type parameter expressions. - /// The rule input exp. - /// - /// + /// The rule to build. + /// The input parameters available to the rule. + /// A delegate that evaluates the rule. + /// Thrown when the rule expression cannot be built. private RuleFunc BuildRuleFunc(Rule rule, RuleParameter[] ruleParams) { var ruleExpressionBuilder = GetExpressionBuilder(rule.RuleExpressionType); @@ -160,15 +166,13 @@ private RuleFunc BuildRuleFunc(Rule rule, RuleParameter[] rulePa } /// - /// Builds the nested expression. + /// Builds a rule function for a nested rule (And/Or) with child rules. /// - /// The parent rule. - /// The child rules. - /// The operation. - /// The type parameter expressions. - /// The rule input exp. - /// Expression of func delegate - /// + /// The parent rule containing child rules. + /// The logical operation to apply (And/AndAlso/Or/OrElse). + /// The input parameters available to the rule. + /// A delegate that evaluates the nested rule. + /// Thrown when child rules cannot be evaluated. private RuleFunc BuildNestedRuleFunc(Rule parentRule, ExpressionType operation, RuleParameter[] ruleParams) { var ruleFuncList = new List>(); @@ -185,6 +189,13 @@ private RuleFunc BuildNestedRuleFunc(Rule parentRule, Expression }; } + /// + /// Applies a logical operation (And/Or) across a collection of rule results. + /// + /// The input parameters. + /// The list of compiled rule functions. + /// The logical expression type. + /// A tuple with the overall success flag and the collected results. private (bool isSuccess ,IEnumerable result) ApplyOperation(RuleParameter[] paramArray,IEnumerable> ruleFuncList, ExpressionType operation) { if (ruleFuncList?.Any() != true) @@ -229,12 +240,27 @@ private RuleFunc BuildNestedRuleFunc(Rule parentRule, Expression return (isSuccess, resultList); } - internal Func> CompileScopedParams(RuleExpressionType ruleExpressionType, RuleParameter[] ruleParameters,RuleExpressionParameter[] ruleExpParams) + /// + /// Compiles scoped parameters into a delegate that evaluates them against input values. + /// + /// The type of rule expression. + /// The input parameters. + /// The scoped expression parameters to compile. + /// A delegate that evaluates scoped parameters and returns a dictionary of values. + private Func> CompileScopedParams(RuleExpressionType ruleExpressionType, RuleParameter[] ruleParameters,RuleExpressionParameter[] ruleExpParams) { return GetExpressionBuilder(ruleExpressionType).CompileScopedParams(ruleParameters, ruleExpParams); } + /// + /// Wraps a compiled rule function with scoped parameter evaluation logic. + /// + /// The rule being wrapped. + /// The compiled rule function. + /// The input parameters. + /// The scoped expression parameters. + /// A wrapped delegate that evaluates scoped params before invoking the rule. private RuleFunc GetWrappedRuleFunc(Rule rule, RuleFunc ruleFunc,RuleParameter[] ruleParameters,RuleExpressionParameter[] ruleExpParams) { if(ruleExpParams.Length == 0) @@ -264,6 +290,11 @@ private RuleFunc GetWrappedRuleFunc(Rule rule, RuleFunc + /// Gets the expression builder for the specified rule expression type. + /// + /// The rule expression type. + /// The expression builder. private RuleExpressionBuilderBase GetExpressionBuilder(RuleExpressionType expressionType) { return _expressionBuilderFactory.RuleGetExpressionBuilder(expressionType); diff --git a/src/RulesEngine/RuleExpressionBuilderFactory.cs b/src/RulesEngine/RuleExpressionBuilderFactory.cs index 3e88f1c5..c22c5c1f 100644 --- a/src/RulesEngine/RuleExpressionBuilderFactory.cs +++ b/src/RulesEngine/RuleExpressionBuilderFactory.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using RulesEngine.ExpressionBuilders; @@ -7,15 +7,31 @@ namespace RulesEngine { + /// + /// A factory that provides the appropriate expression builder for a given rule expression type. + /// internal class RuleExpressionBuilderFactory { private readonly ReSettings _reSettings; private readonly LambdaExpressionBuilder _lambdaExpressionBuilder; + + /// + /// Initializes a new instance of the class. + /// + /// The rules engine settings. + /// The expression parser used by expression builders. public RuleExpressionBuilderFactory(ReSettings reSettings, RuleExpressionParser expressionParser) { _reSettings = reSettings; _lambdaExpressionBuilder = new LambdaExpressionBuilder(_reSettings, expressionParser); } + + /// + /// Gets the expression builder for the specified rule expression type. + /// + /// The type of rule expression. + /// The expression builder capable of building expressions for the specified type. + /// Thrown when the expression type is not supported. public RuleExpressionBuilderBase RuleGetExpressionBuilder(RuleExpressionType ruleExpressionType) { switch (ruleExpressionType) diff --git a/src/RulesEngine/RulesCache.cs b/src/RulesEngine/RulesCache.cs index 7e84ce2a..0b896750 100644 --- a/src/RulesEngine/RulesCache.cs +++ b/src/RulesEngine/RulesCache.cs @@ -10,7 +10,9 @@ namespace RulesEngine { - /// Class RulesCache. + /// + /// Provides caching for workflow definitions and compiled rule delegates. + /// internal class RulesCache { /// The compile rules @@ -19,48 +21,62 @@ internal class RulesCache /// The workflow rules private readonly ConcurrentDictionary _workflow = new ConcurrentDictionary(); + /// + /// Initializes a new instance of the class. + /// + /// The rules engine settings, including cache configuration. public RulesCache(ReSettings reSettings) { _compileRules = new MemCache(reSettings.CacheConfig); } - /// Determines whether [contains workflow rules] [the specified workflow name]. - /// Name of the workflow. - /// - /// true if [contains workflow rules] [the specified workflow name]; otherwise, false. + /// + /// Determines whether a workflow with the specified name is registered. + /// + /// The name of the workflow to check. + /// true if the workflow exists; otherwise, false. public bool ContainsWorkflows(string workflowName) { return _workflow.ContainsKey(workflowName); } + /// + /// Gets a list of all registered workflow names. + /// + /// A list of workflow names. public List GetAllWorkflowNames() { return _workflow.Keys.ToList(); } - /// Adds the or update workflow rules. - /// Name of the workflow. - /// The rules. + /// + /// Adds or updates a workflow definition. + /// + /// The name of the workflow. + /// The workflow definition to store. public void AddOrUpdateWorkflows(string workflowName, Workflow rules) { long ticks = DateTime.UtcNow.Ticks; _workflow.AddOrUpdate(workflowName, (rules, ticks), (k, v) => (rules, ticks)); } - /// Adds the or update compiled rule. - /// The compiled rule key. - /// The compiled rule. + /// + /// Adds or updates compiled rules for a given cache key. + /// + /// The compiled rule cache key. + /// The compiled rules dictionary. public void AddOrUpdateCompiledRule(string compiledRuleKey, IDictionary> compiledRule) { long ticks = DateTime.UtcNow.Ticks; _compileRules.Set(compiledRuleKey, (compiledRule, ticks)); } - /// Checks if the compiled rules are up-to-date. - /// The compiled rule key. - /// The workflow name. - /// - /// true if [compiled rules] is newer than the [workflow rules]; otherwise, false. + /// + /// Checks whether the compiled rules for a cache key are up-to-date with the workflow. + /// + /// The compiled rule cache key. + /// The workflow name to compare against. + /// true if compiled rules are newer or equal to the workflow; otherwise, false. public bool AreCompiledRulesUpToDate(string compiledRuleKey, string workflowName) { if (_compileRules.TryGetValue(compiledRuleKey, out (IDictionary> rules, long tick) compiledRulesObj)) @@ -74,17 +90,21 @@ public bool AreCompiledRulesUpToDate(string compiledRuleKey, string workflowName return false; } - /// Clears this instance. + /// + /// Clears all cached workflows and compiled rules. + /// public void Clear() { _workflow.Clear(); _compileRules.Clear(); } - /// Gets the work flow rules. - /// Name of the workflow. - /// Workflows. - /// Could not find injected Workflow: {wfname} + /// + /// Gets the workflow definition, optionally merging injected workflows. + /// + /// The name of the workflow to retrieve. + /// The workflow definition, or null if not found. + /// Thrown when an injected workflow cannot be found. public Workflow GetWorkflow(string workflowName) { if (_workflow.TryGetValue(workflowName, out (Workflow rules, long tick) WorkflowsObj)) @@ -125,16 +145,20 @@ public Workflow GetWorkflow(string workflowName) } } - /// Gets the compiled rules. - /// The compiled rules key. - /// CompiledRule. + /// + /// Gets compiled rules for the specified cache key. + /// + /// The compiled rules cache key. + /// The compiled rules dictionary, or null if not found. public IDictionary> GetCompiledRules(string compiledRulesKey) { return _compileRules.Get<(IDictionary> rules, long tick)>(compiledRulesKey).rules; } - /// Removes the specified workflow name. - /// Name of the workflow. + /// + /// Removes a workflow and its associated compiled rules from the cache. + /// + /// The name of the workflow to remove. public void Remove(string workflowName) { if (_workflow.TryRemove(workflowName, out var workflowObj)) diff --git a/src/RulesEngine/RulesEngine.cs b/src/RulesEngine/RulesEngine.cs index d7d2389a..2c0fc8b3 100644 --- a/src/RulesEngine/RulesEngine.cs +++ b/src/RulesEngine/RulesEngine.cs @@ -14,14 +14,15 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.RegularExpressions; using System.Threading; +using System.Text.RegularExpressions; using System.Threading.Tasks; namespace RulesEngine { /// - /// + /// Provides the main entry point for executing rule-based workflows. + /// Manages workflow registration, rule compilation, caching, and execution. /// /// public class RulesEngine : IRulesEngine @@ -33,7 +34,7 @@ public class RulesEngine : IRulesEngine private readonly RuleExpressionParser _ruleExpressionParser; private readonly RuleCompiler _ruleCompiler; private readonly ActionFactory _actionFactory; - private const string ParamParseRegex = @"\$\(([^)]+)\)"; + private readonly ErrorMessageFormatter _errorMessageFormatter; private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, @@ -44,6 +45,12 @@ public class RulesEngine : IRulesEngine #region Constructor + /// + /// Initializes a new instance of the class from JSON configuration. + /// + /// An array of JSON strings representing workflow configurations. + /// Optional settings to configure the rules engine behavior. + /// Thrown when a workflow config fails to deserialize. public RulesEngine(string[] jsonConfig, ReSettings reSettings = null) : this(reSettings) { var workflow = jsonConfig.Select((item, index) => { @@ -60,14 +67,23 @@ public RulesEngine(string[] jsonConfig, ReSettings reSettings = null) : this(reS AddWorkflow(workflow); } + /// + /// Initializes a new instance of the class from pre-deserialized workflows. + /// + /// An array of workflows to register. + /// Optional settings to configure the rules engine behavior. public RulesEngine(Workflow[] Workflows, ReSettings reSettings = null) : this(reSettings) { AddWorkflow(Workflows); } + /// + /// Initializes a new instance of the class with optional settings. + /// + /// Optional settings to configure the rules engine behavior. public RulesEngine(ReSettings reSettings = null) { - _reSettings = reSettings == null ? new ReSettings() : new ReSettings(reSettings); + _reSettings = new ReSettings(reSettings ?? new ReSettings()); if (_reSettings.CacheConfig == null) { _reSettings.CacheConfig = new MemCacheConfig(); @@ -76,8 +92,14 @@ public RulesEngine(ReSettings reSettings = null) _ruleExpressionParser = new RuleExpressionParser(_reSettings); _ruleCompiler = new RuleCompiler(new RuleExpressionBuilderFactory(_reSettings, _ruleExpressionParser), _reSettings); _actionFactory = new ActionFactory(GetActionRegistry(_reSettings)); + _errorMessageFormatter = new ErrorMessageFormatter(_reSettings); } + /// + /// Gets the action registry by merging default actions with custom actions from settings. + /// + /// The rules engine settings. + /// A dictionary mapping action names to factory functions. private IDictionary> GetActionRegistry(ReSettings reSettings) { var actionDictionary = GetDefaultActionRegistry(); @@ -101,6 +123,10 @@ private IDictionary> GetActionRegistry(ReSettings reSet /// List of rule results public async ValueTask> ExecuteAllRulesAsync(string workflowName, params object[] inputs) { + if (string.IsNullOrWhiteSpace(workflowName)) + { + throw new ArgumentException($"'{nameof(workflowName)}' cannot be null or whitespace.", nameof(workflowName)); + } var ruleParams = new List(); for (var i = 0; i < inputs.Length; i++) @@ -121,6 +147,10 @@ public async ValueTask> ExecuteAllRulesAsync(string workflo /// List of rule results public async ValueTask> ExecuteAllRulesAsync(string workflowName, CancellationToken cancellationToken, params object[] inputs) { + if (string.IsNullOrWhiteSpace(workflowName)) + { + throw new ArgumentException($"'{nameof(workflowName)}' cannot be null or whitespace.", nameof(workflowName)); + } var ruleParams = new List(); for (var i = 0; i < inputs.Length; i++) @@ -140,11 +170,31 @@ public async ValueTask> ExecuteAllRulesAsync(string workflo /// List of rule results public async ValueTask> ExecuteAllRulesAsync(string workflowName, params RuleParameter[] ruleParams) { + if (string.IsNullOrWhiteSpace(workflowName)) + { + throw new ArgumentException($"'{nameof(workflowName)}' cannot be null or whitespace.", nameof(workflowName)); + } return await ExecuteAllRulesAsync(workflowName, ruleParams, CancellationToken.None); } + /// + /// Executes all rules in the specified workflow with the given parameters. + /// + /// The name of the workflow to execute. + /// The input parameters for rule evaluation. + /// A token to cancel the operation. + /// A list of rule results. public async ValueTask> ExecuteAllRulesAsync(string workflowName, RuleParameter[] ruleParams, CancellationToken cancellationToken) { + if (string.IsNullOrWhiteSpace(workflowName)) + { + throw new ArgumentException($"'{nameof(workflowName)}' cannot be null or whitespace.", nameof(workflowName)); + } + + if (ruleParams == null) + { + throw new ArgumentNullException(nameof(ruleParams)); + } // Copy before sorting to avoid mutating caller's array var sortedParams = new RuleParameter[ruleParams.Length]; Array.Copy(ruleParams, sortedParams, ruleParams.Length); @@ -155,14 +205,53 @@ public async ValueTask> ExecuteAllRulesAsync(string workflo return ruleResultList; } + /// + /// Executes a specific rule within a workflow and returns the action result. + /// + /// The name of the workflow containing the rule. + /// The name of the rule to execute. + /// The input parameters for rule evaluation. + /// The action rule result. public async ValueTask ExecuteActionWorkflowAsync(string workflowName, string ruleName, RuleParameter[] ruleParameters) { + if (string.IsNullOrWhiteSpace(workflowName)) + { + throw new ArgumentException($"'{nameof(workflowName)}' cannot be null or whitespace.", nameof(workflowName)); + } + + if (string.IsNullOrWhiteSpace(ruleName)) + { + throw new ArgumentException($"'{nameof(ruleName)}' cannot be null or whitespace.", nameof(ruleName)); + } + return await ExecuteActionWorkflowAsync(workflowName, ruleName, ruleParameters, CancellationToken.None); } + /// + /// Executes a specific rule within a workflow and returns the action result. + /// + /// The name of the workflow containing the rule. + /// The name of the rule to execute. + /// The input parameters for rule evaluation. + /// A token to cancel the operation. + /// The action rule result. public async ValueTask ExecuteActionWorkflowAsync(string workflowName, string ruleName, RuleParameter[] ruleParameters, CancellationToken cancellationToken) { - var compiledRule = GetCompiledRule(workflowName, ruleName, ruleParameters); + if (string.IsNullOrWhiteSpace(workflowName)) + { + throw new ArgumentException($"'{nameof(workflowName)}' cannot be null or whitespace.", nameof(workflowName)); + } + + if (string.IsNullOrWhiteSpace(ruleName)) + { + throw new ArgumentException($"'{nameof(ruleName)}' cannot be null or whitespace.", nameof(ruleName)); + } + + if (ruleParameters == null) + { + throw new ArgumentNullException(nameof(ruleParameters)); + } + var compiledRule = GetOrCompileRule(workflowName, ruleName, ruleParameters); var resultTree = compiledRule(ruleParameters); return await ExecuteActionForRuleResult(resultTree, true, cancellationToken); } @@ -174,6 +263,10 @@ public async ValueTask ExecuteActionWorkflowAsync(string workf /// public void AddWorkflow(params Workflow[] workflows) { + if (workflows == null) + { + throw new ArgumentNullException(nameof(workflows)); + } try { foreach (var workflow in workflows) @@ -204,6 +297,10 @@ public void AddWorkflow(params Workflow[] workflows) /// public void AddOrUpdateWorkflow(params Workflow[] workflows) { + if (workflows == null) + { + throw new ArgumentNullException(nameof(workflows)); + } try { foreach (var workflow in workflows) @@ -219,6 +316,10 @@ public void AddOrUpdateWorkflow(params Workflow[] workflows) } } + /// + /// Gets a list of all registered workflow names. + /// + /// A list of workflow names. public List GetAllRegisteredWorkflowNames() { return _rulesCache.GetAllWorkflowNames(); @@ -231,6 +332,10 @@ public List GetAllRegisteredWorkflowNames() /// true if contains the specified workflow name; otherwise, false. public bool ContainsWorkflow(string workflowName) { + if (string.IsNullOrWhiteSpace(workflowName)) + { + throw new ArgumentException($"'{nameof(workflowName)}' cannot be null or whitespace.", nameof(workflowName)); + } return _rulesCache.ContainsWorkflows(workflowName); } @@ -248,6 +353,10 @@ public void ClearWorkflows() /// The workflow names. public void RemoveWorkflow(params string[] workflowNames) { + if (workflowNames == null) + { + throw new ArgumentNullException(nameof(workflowNames)); + } foreach (var workflowName in workflowNames) { _rulesCache.Remove(workflowName); @@ -258,6 +367,11 @@ public void RemoveWorkflow(params string[] workflowNames) #region Private Methods + /// + /// Executes actions for a collection of rule results, including nested child results. + /// + /// The rule results to process. + /// A token to cancel the operation. private async ValueTask ExecuteActionAsync(IEnumerable ruleResultList, CancellationToken cancellationToken = default) { foreach (var ruleResult in ruleResultList) @@ -273,6 +387,13 @@ private async ValueTask ExecuteActionAsync(IEnumerable ruleResul } } + /// + /// Executes the action configured for a single rule result (OnSuccess or OnFailure). + /// + /// The rule result to process. + /// Whether to include rule results in the output. + /// A token to cancel the operation. + /// The action execution result. private async ValueTask ExecuteActionForRuleResult(RuleResultTree resultTree, bool includeRuleResults = false, CancellationToken cancellationToken = default) { var ruleActions = resultTree?.Rule?.Actions; @@ -295,13 +416,13 @@ private async ValueTask ExecuteActionForRuleResult(RuleResultT } /// - /// This will validate workflow rules then call execute method + /// Validates the workflow exists, compiles rules if needed, and executes all rules. /// - /// type of entity - /// input - /// workflow name - /// list of rule result set - internal List ValidateWorkflowAndExecuteRule(string workflowName, RuleParameter[] ruleParams) + /// The name of the workflow. + /// The input parameters for rule evaluation. + /// A list of rule results. + /// Thrown when the workflow is not registered. + private List ValidateWorkflowAndExecuteRule(string workflowName, RuleParameter[] ruleParams) { List result; @@ -318,13 +439,11 @@ internal List ValidateWorkflowAndExecuteRule(string workflowName } /// - /// This will compile the rules and store them to dictionary + /// Compiles and caches rules for the specified workflow if not already up-to-date. /// - /// workflow name - /// The rule parameters. - /// - /// bool result - /// + /// The name of the workflow. + /// The input parameters for rule evaluation. + /// true if registration succeeded; otherwise, false. private bool RegisterRule(string workflowName, params RuleParameter[] ruleParams) { var compileRulesKey = GetCompiledRulesKey(workflowName, ruleParams); @@ -349,7 +468,7 @@ private bool RegisterRule(string workflowName, params RuleParameter[] ruleParams foreach (var rule in workflow.Rules.Where(c => c.Enabled)) { - dictFunc.Add(rule.RuleName, CompileRule(rule, workflow.RuleExpressionType, ruleParams, globalParamExp)); + dictFunc.Add(rule.RuleName, CompileRuleInternal(rule, workflow.RuleExpressionType, ruleParams, globalParamExp)); } _rulesCache.AddOrUpdateCompiledRule(compileRulesKey, dictFunc); @@ -361,7 +480,15 @@ private bool RegisterRule(string workflowName, params RuleParameter[] ruleParams } } - private RuleFunc GetCompiledRule(string workflowName, string ruleName, RuleParameter[] ruleParameters) + /// + /// Gets a compiled rule from cache, or compiles it on demand if not found. + /// + /// The name of the workflow. + /// The name of the rule. + /// The input parameters for rule evaluation. + /// The compiled rule delegate. + /// Thrown when the workflow or rule is not found. + private RuleFunc GetOrCompileRule(string workflowName, string ruleName, RuleParameter[] ruleParameters) { // Ensure the workflow is registered and rules are compiled if (!RegisterRule(workflowName, ruleParameters)) @@ -380,10 +507,18 @@ private RuleFunc GetCompiledRule(string workflowName, string rul // Fallback to individual compilation if not found in cache // This should rarely happen, but provides safety - return CompileRule(workflowName, ruleName, ruleParameters); + return CompileRuleByName(workflowName, ruleName, ruleParameters); } - private RuleFunc CompileRule(string workflowName, string ruleName, RuleParameter[] ruleParameters) + /// + /// Compiles a rule by looking it up from the workflow by name. + /// + /// The name of the workflow. + /// The name of the rule to compile. + /// The input parameters for rule evaluation. + /// The compiled rule delegate. + /// Thrown when the workflow or rule is not found. + private RuleFunc CompileRuleByName(string workflowName, string ruleName, RuleParameter[] ruleParameters) { var workflow = _rulesCache.GetWorkflow(workflowName); if (workflow == null) @@ -398,10 +533,18 @@ private RuleFunc CompileRule(string workflowName, string ruleNam var globalParamExp = new Lazy( () => _ruleCompiler.GetRuleExpressionParameters(workflow.RuleExpressionType, workflow.GlobalParams, ruleParameters) ); - return CompileRule(currentRule, workflow.RuleExpressionType, ruleParameters, globalParamExp); + return CompileRuleInternal(currentRule, workflow.RuleExpressionType, ruleParameters, globalParamExp); } - private RuleFunc CompileRule(Rule rule, RuleExpressionType ruleExpressionType, RuleParameter[] ruleParams, Lazy scopedParams) + /// + /// Delegates rule compilation to the . + /// + /// The rule to compile. + /// The type of rule expression. + /// The input parameters for rule evaluation. + /// Lazy-loaded scoped expression parameters. + /// The compiled rule delegate. + private RuleFunc CompileRuleInternal(Rule rule, RuleExpressionType ruleExpressionType, RuleParameter[] ruleParams, Lazy scopedParams) { return _ruleCompiler.CompileRule(rule, ruleExpressionType, ruleParams, scopedParams); } @@ -409,8 +552,8 @@ private RuleFunc CompileRule(Rule rule, RuleExpressionType ruleE /// /// This will execute the compiled rules /// - /// - /// + /// The name of the workflow. + /// The input parameters for rule evaluation. /// list of rule result set private List ExecuteAllRuleByWorkflow(string workflowName, RuleParameter[] ruleParameters) { @@ -435,7 +578,7 @@ private List ExecuteAllRuleByWorkflow(string workflowName, RuleP if (hasRuleReferences && _reSettings.EnableScopedParams) { // Compile rule with additional scoped parameters for rule results - compiledRule = CompileRuleWithRuleResults(rule, workflow.RuleExpressionType, extendedRuleParameters.ToArray(), ruleResults, successEvents, workflow); + compiledRule = CompileRuleWithReferences(rule, workflow.RuleExpressionType, extendedRuleParameters.ToArray(), ruleResults, successEvents, workflow); } else { @@ -450,7 +593,7 @@ private List ExecuteAllRuleByWorkflow(string workflowName, RuleP var globalParamExp = new Lazy( () => _ruleCompiler.GetRuleExpressionParameters(workflow.RuleExpressionType, workflow.GlobalParams, ruleParameters) ); - compiledRule = CompileRule(rule, workflow.RuleExpressionType, ruleParameters, globalParamExp); + compiledRule = CompileRuleInternal(rule, workflow.RuleExpressionType, ruleParameters, globalParamExp); } } @@ -467,14 +610,22 @@ private List ExecuteAllRuleByWorkflow(string workflowName, RuleP } } - FormatErrorMessages(result); + _errorMessageFormatter.FormatErrorMessages(result); return result; } + /// + /// Checks if the expression contains references to other rules (e.g., @RuleName). + /// + /// The rule expression to check. + /// The names of rules that have been evaluated so far. + /// true if the expression contains a rule reference; otherwise, false. private bool ContainsRuleReferences(string expression, IEnumerable availableRuleNames) { if (string.IsNullOrEmpty(expression)) + { return false; + } foreach (var ruleName in availableRuleNames) { @@ -487,10 +638,18 @@ private bool ContainsRuleReferences(string expression, IEnumerable avail return false; } + /// + /// Checks if the expression contains references to success events. + /// + /// The rule expression to check. + /// The success events that have been collected so far. + /// true if the expression contains a success event reference; otherwise, false. private bool ContainsSuccessEventReferences(string expression, IEnumerable availableSuccessEvents) { if (string.IsNullOrEmpty(expression)) + { return false; + } foreach (var eventName in availableSuccessEvents) { @@ -503,7 +662,17 @@ private bool ContainsSuccessEventReferences(string expression, IEnumerable CompileRuleWithRuleResults(Rule rule, RuleExpressionType ruleExpressionType, RuleParameter[] ruleParameters, Dictionary ruleResults, HashSet successEvents, Workflow workflow = null) + /// + /// Compiles a rule that references other rule results or success events. + /// + /// The rule to compile. + /// The type of rule expression. + /// The input parameters for rule evaluation. + /// Previously evaluated rule results. + /// Previously collected success events. + /// The parent workflow for global parameters. + /// The compiled rule delegate with reference resolution. + private RuleFunc CompileRuleWithReferences(Rule rule, RuleExpressionType ruleExpressionType, RuleParameter[] ruleParameters, Dictionary ruleResults, HashSet successEvents, Workflow workflow = null) { var globalParamExp = new Lazy( () => _ruleCompiler.GetRuleExpressionParameters(ruleExpressionType, workflow?.GlobalParams, ruleParameters) @@ -562,9 +731,15 @@ private RuleFunc CompileRuleWithRuleResults(Rule rule, RuleExpre WorkflowsToInject = rule.WorkflowsToInject }; - return CompileRule(modifiedRule, ruleExpressionType, ruleParameters, globalParamExp); + return CompileRuleInternal(modifiedRule, ruleExpressionType, ruleParameters, globalParamExp); } + /// + /// Generates a cache key for compiled rules based on workflow and parameter types. + /// + /// The name of the workflow. + /// The input parameters for rule evaluation. + /// A unique cache key string. private string GetCompiledRulesKey(string workflowName, RuleParameter[] ruleParams) { // Use FullName instead of Name to avoid collisions @@ -573,6 +748,10 @@ private string GetCompiledRulesKey(string workflowName, RuleParameter[] rulePara return key; } + /// + /// Gets the default action registry with built-in actions. + /// + /// A dictionary mapping action names to factory functions. private IDictionary> GetDefaultActionRegistry() { return new Dictionary>{ @@ -581,75 +760,6 @@ private IDictionary> GetDefaultActionRegistry() }; } - /// - /// 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 ? JsonSerializer.Serialize(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 model = inputs?.FirstOrDefault(c => string.Equals(c.Key, typeName)).Value; - - if (model != null) - { - using (var jDoc = JsonSerializer.SerializeToDocument(model)) - { - errorMessage = jDoc.RootElement.TryGetProperty(propertyName, out var jElement) ? - errorMessage.Replace($"$({property})", jElement.GetRawText() ?? $"({property})") : - errorMessage.Replace($"$({property})", $"({property})"); - } - } - - return errorMessage; - } - #endregion } } \ No newline at end of file diff --git a/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs index dcad791c..07ecf247 100644 --- a/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs +++ b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs @@ -397,10 +397,9 @@ public async Task ExecuteRule_WithInjectedUtils_ReturnsListOfRuleResultTree(stri input1.Property1 = propValue; - var utils = new TestInstanceUtils(); - var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1), new RuleParameter("utils", utils)); + var result = await re.ExecuteAllRulesAsync("inputWorkflow", new[] { new RuleParameter("input1", input1), new RuleParameter("utils", utils) }); Assert.NotNull(result); Assert.IsType>(result); Assert.All(result, c => Assert.Equal(expectedResult, c.IsSuccess));