From 571490455cffe7aa2b219f5925e27ffe286da2b1 Mon Sep 17 00:00:00 2001 From: Abbas Cyclewala Date: Tue, 12 Apr 2022 19:24:16 +0530 Subject: [PATCH] Abbasc52/combined fixes (#341) * Fixed actions not working with complex objects * removed Microsoft.Extensions.Caching dependency * * updated nuget packages * update dotnet version to 6 for demo/test proj * improved integration with DynamicLinq * added test case for jsonElement getProperty method * updated test cases to cover case insensitivity * updated workflow schema * added schema for list of workflows * fixed schema name and added to solution * Update dotnetcore-build.yml * Update dotnetcore-build.yml * updated unit test proj to point to dotnet 6 * removed ilogger --- .github/dependabot.yml | 6 +- .github/workflows/dotnetcore-build.yml | 6 +- RulesEngine.sln | 1 + benchmark/RulesEngineBenchmark/Program.cs | 2 +- .../RulesEngineBenchmark.csproj | 2 +- .../DemoApp.EFDataExample.csproj | 6 +- demo/DemoApp.EFDataExample/RulesEngineContext.cs | 10 +- demo/DemoApp/DemoApp.csproj | 2 +- global.json | 2 +- schema/workflow-list-schema.json | 7 + schema/workflow-schema.json | 49 +- .../ExpressionBuilders/LambdaExpressionBuilder.cs | 2 +- .../RuleExpressionBuilderBase.cs | 2 +- .../ExpressionBuilders/RuleExpressionParser.cs | 31 +- src/RulesEngine/HelperFunctions/ExpressionUtils.cs | 2 +- src/RulesEngine/HelperFunctions/MemCache.cs | 115 ++ src/RulesEngine/Models/ReSettings.cs | 3 + src/RulesEngine/Models/Workflow.cs | 14 +- src/RulesEngine/RuleCompiler.cs | 17 +- src/RulesEngine/RulesCache.cs | 41 +- src/RulesEngine/RulesEngine.cs | 854 +++++----- src/RulesEngine/RulesEngine.csproj | 10 +- .../ActionTests/CustomActionTest.cs | 4 +- .../ActionTests/RulesEngineWithActionsTests.cs | 23 + .../RulesEngine.UnitTest/BusinessRuleEngineTest.cs | 1746 ++++++++++---------- test/RulesEngine.UnitTest/RuleCompilerTest.cs | 9 +- .../RulesEngine.UnitTest.csproj | 13 +- test/RulesEngine.UnitTest/ScopedParamsTest.cs | 6 +- test/RulesEngine.UnitTest/TestData/rules10.json | 11 + test/RulesEngine.UnitTest/TestData/rules5.json | 10 +- 30 files changed, 1603 insertions(+), 1403 deletions(-) create mode 100644 schema/workflow-list-schema.json create mode 100644 src/RulesEngine/HelperFunctions/MemCache.cs create mode 100644 test/RulesEngine.UnitTest/TestData/rules10.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d1ab6ae..1d9c02a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,7 +3,7 @@ updates: - package-ecosystem: "github-actions" # default location of `.github/workflows` directory: "/" - open-pull-requests-limit: 10 + open-pull-requests-limit: 3 schedule: interval: "weekly" # assignees: @@ -14,9 +14,9 @@ updates: - package-ecosystem: "nuget" # location of package manifests directory: "/" - open-pull-requests-limit: 10 + open-pull-requests-limit: 3 schedule: - interval: "daily" + interval: "weekly" ignore: - dependency-name: "*" update-types: ["version-update:semver-minor"] diff --git a/.github/workflows/dotnetcore-build.yml b/.github/workflows/dotnetcore-build.yml index d8f68a3..5f574b7 100644 --- a/.github/workflows/dotnetcore-build.yml +++ b/.github/workflows/dotnetcore-build.yml @@ -31,12 +31,12 @@ jobs: - uses: actions/checkout@v2 - name: Setup .NET Core - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v2 with: - dotnet-version: 3.1 + dotnet-version: 6.0.x - name: Install minicover - run: dotnet tool install --global minicover --version 3.0.6 + run: dotnet tool install --global minicover --version 3.4.4 - name: Install dependencies run: dotnet restore RulesEngine.sln diff --git a/RulesEngine.sln b/RulesEngine.sln index 1bb69ef..fbad613 100644 --- a/RulesEngine.sln +++ b/RulesEngine.sln @@ -18,6 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution CHANGELOG.md = CHANGELOG.md global.json = global.json README.md = README.md + schema\workflow-list-schema.json = schema\workflow-list-schema.json schema\workflow-schema.json = schema\workflow-schema.json EndProjectSection EndProject diff --git a/benchmark/RulesEngineBenchmark/Program.cs b/benchmark/RulesEngineBenchmark/Program.cs index 6bbfe2f..6d611ab 100644 --- a/benchmark/RulesEngineBenchmark/Program.cs +++ b/benchmark/RulesEngineBenchmark/Program.cs @@ -36,7 +36,7 @@ namespace RulesEngineBenchmark var fileData = File.ReadAllText(files[0]); workflow = JsonConvert.DeserializeObject>(fileData); - rulesEngine = new RulesEngine.RulesEngine(workflow.ToArray(), null, new ReSettings { + rulesEngine = new RulesEngine.RulesEngine(workflow.ToArray(), new ReSettings { EnableFormattedErrorMessage = false, EnableScopedParams = false }); diff --git a/benchmark/RulesEngineBenchmark/RulesEngineBenchmark.csproj b/benchmark/RulesEngineBenchmark/RulesEngineBenchmark.csproj index 0e71ec3..e7acc8c 100644 --- a/benchmark/RulesEngineBenchmark/RulesEngineBenchmark.csproj +++ b/benchmark/RulesEngineBenchmark/RulesEngineBenchmark.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6.0 diff --git a/demo/DemoApp.EFDataExample/DemoApp.EFDataExample.csproj b/demo/DemoApp.EFDataExample/DemoApp.EFDataExample.csproj index 54d3449..19c8bda 100644 --- a/demo/DemoApp.EFDataExample/DemoApp.EFDataExample.csproj +++ b/demo/DemoApp.EFDataExample/DemoApp.EFDataExample.csproj @@ -1,14 +1,14 @@  - netcoreapp3.1 + net6.0 DemoApp.EFDataExample DemoApp.EFDataExample - - + + diff --git a/demo/DemoApp.EFDataExample/RulesEngineContext.cs b/demo/DemoApp.EFDataExample/RulesEngineContext.cs index 64ed43e..f2b94d3 100644 --- a/demo/DemoApp.EFDataExample/RulesEngineContext.cs +++ b/demo/DemoApp.EFDataExample/RulesEngineContext.cs @@ -24,18 +24,20 @@ namespace RulesEngine.Data entity.Ignore(b => b.WorkflowsToInject); }); + var serializationOptions = new JsonSerializerOptions(JsonSerializerDefaults.General); + modelBuilder.Entity(entity => { entity.HasKey(k => k.RuleName); entity.Property(b => b.Properties) .HasConversion( - v => JsonSerializer.Serialize(v, null), - v => JsonSerializer.Deserialize>(v, null)); + v => JsonSerializer.Serialize(v, serializationOptions), + v => JsonSerializer.Deserialize>(v, serializationOptions)); ; entity.Property(p => p.Actions) .HasConversion( - v => JsonSerializer.Serialize(v, null), - v => JsonSerializer.Deserialize(v, null)); + v => JsonSerializer.Serialize(v, serializationOptions), + v => JsonSerializer.Deserialize(v, serializationOptions)); entity.Ignore(b => b.WorkflowsToInject); }); diff --git a/demo/DemoApp/DemoApp.csproj b/demo/DemoApp/DemoApp.csproj index 4e88a3d..fb6e5d9 100644 --- a/demo/DemoApp/DemoApp.csproj +++ b/demo/DemoApp/DemoApp.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6.0 DemoApp.Program diff --git a/global.json b/global.json index d36fe28..50f074c 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "3.1", + "version": "6.0", "rollForward": "latestFeature", "allowPrerelease": false } diff --git a/schema/workflow-list-schema.json b/schema/workflow-list-schema.json new file mode 100644 index 0000000..435e071 --- /dev/null +++ b/schema/workflow-list-schema.json @@ -0,0 +1,7 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "array", + "items": { + "$ref": "https://raw.githubusercontent.com/microsoft/RulesEngine/main/schema/workflow-schema.json" + } +} \ No newline at end of file diff --git a/schema/workflow-schema.json b/schema/workflow-schema.json index 6d7ea7a..94887d0 100644 --- a/schema/workflow-schema.json +++ b/schema/workflow-schema.json @@ -1,12 +1,24 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { + "ScopedParam": { + "type": "object", + "properties": { + "Name": { "type": "string" }, + "Expression": { "type": "string" } + }, + "required": [ "Name", "Expression" ] + }, "Rule": { "title": "Rule", "properties": { "RuleName": { "type": "string" }, + "LocalParams": { + "type": "array", + "items": { "$ref": "#/definitions/ScopedParam" } + }, "Operator": { "enum": [ "And", @@ -18,12 +30,6 @@ "ErrorMessage": { "type": "string" }, - "ErrorType": { - "enum": [ - "Warning", - "Error" - ] - }, "SuccessEvent": { "type": "string" }, @@ -45,6 +51,10 @@ }, "Actions": { "$ref": "#/definitions/RuleActions" + }, + "Enabled": { + "type": "boolean", + "default": true } }, "required": [ @@ -65,6 +75,10 @@ "RuleName": { "type": "string" }, + "LocalParams": { + "type": "array", + "items": { "$ref": "#/definitions/ScopedParam" } + }, "Expression": { "type": "string" }, @@ -76,12 +90,6 @@ "ErrorMessage": { "type": "string" }, - "ErrorType": { - "enum": [ - "Warning", - "Error" - ] - }, "SuccessEvent": { "type": "string" }, @@ -90,6 +98,10 @@ }, "Actions": { "$ref": "#/definitions/RuleActions" + }, + "Enabled": { + "type": "boolean", + "default": true } } }, @@ -116,11 +128,20 @@ } } } + }, "properties": { - "WorkFlowName": { + "WorkflowName": { "type": "string" }, + "WorkflowsToInject": { + "type": "array", + "items": { "type": "string" } + }, + "GlobalParams": { + "type": "array", + "items": { "$ref": "#/definitions/ScopedParam" } + }, "Rules": { "type": "array", "items": { @@ -136,7 +157,7 @@ } }, "required": [ - "WorkFlowName", + "WorkflowName", "Rules" ], "type": "object" diff --git a/src/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs b/src/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs index 2a7cbb5..b35961b 100644 --- a/src/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs +++ b/src/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs @@ -43,7 +43,7 @@ namespace RulesEngine.ExpressionBuilders } } - internal override LambdaExpression Parse(string expression, ParameterExpression[] parameters, Type returnType) + internal override Expression Parse(string expression, ParameterExpression[] parameters, Type returnType) { try { diff --git a/src/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs b/src/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs index 61c1c5d..854ea79 100644 --- a/src/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs +++ b/src/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs @@ -22,7 +22,7 @@ namespace RulesEngine.ExpressionBuilders /// Expression type internal abstract RuleFunc BuildDelegateForRule(Rule rule, RuleParameter[] ruleParams); - internal abstract LambdaExpression Parse(string expression, ParameterExpression[] parameters, Type returnType); + internal abstract Expression Parse(string expression, ParameterExpression[] parameters, Type returnType); internal abstract Func> CompileScopedParams(RuleParameter[] ruleParameters, RuleExpressionParameter[] scopedParameters); } diff --git a/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs b/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs index f856675..ac43d10 100644 --- a/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs +++ b/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs @@ -2,12 +2,13 @@ // Licensed under the MIT License. using FastExpressionCompiler; -using Microsoft.Extensions.Caching.Memory; +using RulesEngine.HelperFunctions; using RulesEngine.Models; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Dynamic.Core; +using System.Linq.Dynamic.Core.Parser; using System.Linq.Expressions; using System.Reflection; @@ -16,13 +17,13 @@ namespace RulesEngine.ExpressionBuilders public class RuleExpressionParser { private readonly ReSettings _reSettings; - private static IMemoryCache _memoryCache; + private static MemCache _memoryCache; private readonly IDictionary _methodInfo; public RuleExpressionParser(ReSettings reSettings) { _reSettings = reSettings; - _memoryCache = _memoryCache ?? new MemoryCache(new MemoryCacheOptions { + _memoryCache = _memoryCache ?? new MemCache(new MemCacheConfig { SizeLimit = 1000 }); _methodInfo = new Dictionary(); @@ -34,22 +35,30 @@ namespace RulesEngine.ExpressionBuilders 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 LambdaExpression Parse(string expression, ParameterExpression[] parameters, Type returnType) + public Expression Parse(string expression, ParameterExpression[] parameters, Type returnType) { var config = new ParsingConfig { CustomTypeProvider = new CustomTypeProvider(_reSettings.CustomTypes) }; + return new ExpressionParser(parameters, expression, new object[] { }, config).Parse(returnType); - return DynamicExpressionParser.ParseLambda(config, false, parameters, returnType, expression); } public Func Compile(string expression, RuleParameter[] ruleParams) - { + { + var rtype = typeof(T); + if(rtype == typeof(object)) + { + rtype = null; + } var cacheKey = GetCacheKey(expression, ruleParams, typeof(T)); - return _memoryCache.GetOrCreate(cacheKey, (entry) => { - entry.SetSize(1); + return _memoryCache.GetOrCreate(cacheKey, () => { var parameterExpressions = GetParameterExpression(ruleParams).ToArray(); - var e = Parse(expression, parameterExpressions, typeof(T)); - var expressionBody = new List() { e.Body }; + var e = Parse(expression, parameterExpressions, rtype); + if(rtype == null) + { + e = Expression.Convert(e, typeof(T)); + } + var expressionBody = new List() { e }; var wrappedExpression = WrapExpression(expressionBody, parameterExpressions, new ParameterExpression[] { }); return wrappedExpression.CompileFast(); }); @@ -75,7 +84,7 @@ namespace RulesEngine.ExpressionBuilders } public T Evaluate(string expression, RuleParameter[] ruleParams) - { + { var func = Compile(expression, ruleParams); return func(ruleParams.Select(c => c.Value).ToArray()); } diff --git a/src/RulesEngine/HelperFunctions/ExpressionUtils.cs b/src/RulesEngine/HelperFunctions/ExpressionUtils.cs index d725cf0..d8ca863 100644 --- a/src/RulesEngine/HelperFunctions/ExpressionUtils.cs +++ b/src/RulesEngine/HelperFunctions/ExpressionUtils.cs @@ -10,7 +10,7 @@ namespace RulesEngine.HelperFunctions { public static bool CheckContains(string check, string valList) { - if (String.IsNullOrEmpty(check) || String.IsNullOrEmpty(valList)) + if (string.IsNullOrEmpty(check) || string.IsNullOrEmpty(valList)) return false; var list = valList.Split(',').ToList(); diff --git a/src/RulesEngine/HelperFunctions/MemCache.cs b/src/RulesEngine/HelperFunctions/MemCache.cs new file mode 100644 index 0000000..74abe2b --- /dev/null +++ b/src/RulesEngine/HelperFunctions/MemCache.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace RulesEngine.HelperFunctions +{ + public class MemCacheConfig { + public int SizeLimit { get; set; } = 1000; + } + + + internal class MemCache + { + private readonly MemCacheConfig _config; + private ConcurrentDictionary _cacheDictionary; + private ConcurrentQueue<(string key, DateTimeOffset expiry)> _cacheEvictionQueue; + + public MemCache(MemCacheConfig config) + { + if(config == null) + { + config = new MemCacheConfig(); + } + _config = config; + _cacheDictionary = new ConcurrentDictionary(); + _cacheEvictionQueue = new ConcurrentQueue<(string key, DateTimeOffset expiry)>(); + } + + public bool TryGetValue(string key,out T value) + { + value = default; + if (_cacheDictionary.TryGetValue(key, out var cacheItem)) + { + if(cacheItem.expiry < DateTimeOffset.UtcNow) + { + _cacheDictionary.TryRemove(key, out _); + return false; + } + else + { + value = (T)cacheItem.value; + return true; + } + } + return false; + + } + + + public T Get(string key) + { + TryGetValue(key, out var value); + return value; + } + + + /// + /// Returns all known keys. May return keys for expired data as well + /// + /// + public IEnumerable GetKeys() + { + return _cacheDictionary.Keys; + } + + public T GetOrCreate(string key, Func createFn, DateTimeOffset? expiry = null) + { + if(!TryGetValue(key,out var value)) + { + value = createFn(); + return Set(key,value,expiry); + } + return value; + } + + public T Set(string key, T value, DateTimeOffset? expiry = null) + { + var fixedExpiry = expiry ?? DateTimeOffset.MaxValue; + + while (_cacheDictionary.Count > _config.SizeLimit) + { + if (_cacheEvictionQueue.IsEmpty) + { + _cacheDictionary.Clear(); + } + if(_cacheEvictionQueue.TryDequeue(out var result) + && _cacheDictionary.TryGetValue(result.key,out var dictionaryValue) + && dictionaryValue.expiry == result.expiry) + { + _cacheDictionary.TryRemove(result.key, out _); + } + + } + + _cacheDictionary.AddOrUpdate(key, (value, fixedExpiry), (k, v) => (value, fixedExpiry)); + _cacheEvictionQueue.Enqueue((key, fixedExpiry)); + return value; + } + + public void Remove(string key) + { + _cacheDictionary.TryRemove(key, out _); + } + + public void Clear() + { + _cacheDictionary.Clear(); + _cacheEvictionQueue = new ConcurrentQueue<(string key, DateTimeOffset expiry)>(); + } + } +} diff --git a/src/RulesEngine/Models/ReSettings.cs b/src/RulesEngine/Models/ReSettings.cs index 33dd7c0..51aeedb 100644 --- a/src/RulesEngine/Models/ReSettings.cs +++ b/src/RulesEngine/Models/ReSettings.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using RulesEngine.Actions; +using RulesEngine.HelperFunctions; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -57,6 +58,8 @@ namespace RulesEngine.Models get { return EnableScopedParams; } set { EnableScopedParams = value; } } + + public MemCacheConfig CacheConfig { get; set; } } public enum NestedRuleExecutionMode diff --git a/src/RulesEngine/Models/Workflow.cs b/src/RulesEngine/Models/Workflow.cs index 7e38875..c264a5e 100644 --- a/src/RulesEngine/Models/Workflow.cs +++ b/src/RulesEngine/Models/Workflow.cs @@ -23,13 +23,13 @@ namespace RulesEngine.Models /// public string WorkflowName { get; set; } - /// Gets or sets the workflow rules to inject. - /// The workflow rules to inject. - [Obsolete("WorkflowRulesToInject is deprecated. Use WorkflowsToInject instead.")] - public IEnumerable WorkflowRulesToInject { - set { WorkflowsToInject = value; } - } - public IEnumerable WorkflowsToInject { get; set; } + /// Gets or sets the workflow rules to inject. + /// The workflow rules to inject. + [Obsolete("WorkflowRulesToInject is deprecated. Use WorkflowsToInject instead.")] + public IEnumerable WorkflowRulesToInject { + set { WorkflowsToInject = value; } + } + public IEnumerable WorkflowsToInject { get; set; } /// /// Gets or Sets the global params which will be applicable to all rules diff --git a/src/RulesEngine/RuleCompiler.cs b/src/RulesEngine/RuleCompiler.cs index 907296d..41e40e2 100644 --- a/src/RulesEngine/RuleCompiler.cs +++ b/src/RulesEngine/RuleCompiler.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.Extensions.Logging; using RulesEngine.Exceptions; using RulesEngine.ExpressionBuilders; using RulesEngine.HelperFunctions; @@ -10,7 +9,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Runtime.InteropServices; namespace RulesEngine { @@ -30,20 +28,13 @@ namespace RulesEngine private readonly RuleExpressionBuilderFactory _expressionBuilderFactory; private readonly ReSettings _reSettings; - /// - /// The logger - /// - private readonly ILogger _logger; - /// /// Initializes a new instance of the class. /// /// The expression builder factory. /// expressionBuilderFactory - internal RuleCompiler(RuleExpressionBuilderFactory expressionBuilderFactory, ReSettings reSettings, ILogger logger) + internal RuleCompiler(RuleExpressionBuilderFactory expressionBuilderFactory, ReSettings reSettings) { - _logger = logger ?? throw new ArgumentNullException($"{nameof(logger)} can't be null."); - _expressionBuilderFactory = expressionBuilderFactory ?? throw new ArgumentNullException($"{nameof(expressionBuilderFactory)} can't be null."); _reSettings = reSettings; } @@ -61,7 +52,6 @@ namespace RulesEngine if (rule == null) { var ex = new ArgumentNullException(nameof(rule)); - _logger.LogError(ex.Message); throw ex; } try @@ -75,7 +65,6 @@ namespace RulesEngine catch (Exception ex) { var message = $"Error while compiling rule `{rule.RuleName}`: {ex.Message}"; - _logger.LogError(message); return Helpers.ToRuleExceptionResult(_reSettings, rule, new RuleException(message, ex)); } } @@ -131,7 +120,7 @@ namespace RulesEngine { try { - var lpExpression = expressionBuilder.Parse(lp.Expression, parameters.ToArray(), null).Body; + var lpExpression = expressionBuilder.Parse(lp.Expression, parameters.ToArray(), null); var ruleExpParam = new RuleExpressionParameter() { ParameterExpression = Expression.Parameter(lpExpression.Type, lp.Name), ValueExpression = lpExpression @@ -187,7 +176,7 @@ namespace RulesEngine return (paramArray) => { var (isSuccess, resultList) = ApplyOperation(paramArray, ruleFuncList, operation); - Func isSuccessFn = (p) => isSuccess; + bool isSuccessFn(object[] p) => isSuccess; var result = Helpers.ToResultTree(_reSettings, parentRule, resultList, isSuccessFn); return result(paramArray); }; diff --git a/src/RulesEngine/RulesCache.cs b/src/RulesEngine/RulesCache.cs index 0d17d3e..9f0bc30 100644 --- a/src/RulesEngine/RulesCache.cs +++ b/src/RulesEngine/RulesCache.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using RulesEngine.HelperFunctions; using RulesEngine.Models; using System; using System.Collections.Concurrent; @@ -13,10 +14,17 @@ namespace RulesEngine internal class RulesCache { /// The compile rules - private ConcurrentDictionary>, Int64)> _compileRules = new ConcurrentDictionary>, Int64)>(); + private readonly MemCache _compileRules; /// The workflow rules - private ConcurrentDictionary _workflow = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _workflow = new ConcurrentDictionary(); + + + public RulesCache(ReSettings reSettings) + { + _compileRules = new MemCache(reSettings.CacheConfig); + } + /// Determines whether [contains workflow rules] [the specified workflow name]. /// Name of the workflow. @@ -32,21 +40,12 @@ namespace RulesEngine return _workflow.Keys.ToList(); } - /// 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 AddOrUpdateWorkflows(string workflowName, Workflow rules) { - Int64 ticks = DateTime.UtcNow.Ticks; + long ticks = DateTime.UtcNow.Ticks; _workflow.AddOrUpdate(workflowName, (rules, ticks), (k, v) => (rules, ticks)); } @@ -55,8 +54,8 @@ namespace RulesEngine /// The compiled rule. public void AddOrUpdateCompiledRule(string compiledRuleKey, IDictionary> compiledRule) { - Int64 ticks = DateTime.UtcNow.Ticks; - _compileRules.AddOrUpdate(compiledRuleKey, (compiledRule, ticks), (k, v) => (compiledRule, ticks)); + long ticks = DateTime.UtcNow.Ticks; + _compileRules.Set(compiledRuleKey,(compiledRule, ticks)); } /// Checks if the compiled rules are up-to-date. @@ -66,9 +65,9 @@ namespace RulesEngine /// true if [compiled rules] is newer than the [workflow rules]; otherwise, false. public bool AreCompiledRulesUpToDate(string compiledRuleKey, string workflowName) { - if (_compileRules.TryGetValue(compiledRuleKey, out (IDictionary> rules, Int64 tick) compiledRulesObj)) + if (_compileRules.TryGetValue(compiledRuleKey, out (IDictionary> rules, long tick) compiledRulesObj)) { - if (_workflow.TryGetValue(workflowName, out (Workflow rules, Int64 tick) WorkflowsObj)) + if (_workflow.TryGetValue(workflowName, out (Workflow rules, long tick) WorkflowsObj)) { return compiledRulesObj.tick >= WorkflowsObj.tick; } @@ -90,7 +89,7 @@ namespace RulesEngine /// Could not find injected Workflow: {wfname} public Workflow GetWorkflow(string workflowName) { - if (_workflow.TryGetValue(workflowName, out (Workflow rules, Int64 tick) WorkflowsObj)) + if (_workflow.TryGetValue(workflowName, out (Workflow rules, long tick) WorkflowsObj)) { var workflow = WorkflowsObj.rules; if (workflow.WorkflowsToInject?.Any() == true) @@ -125,19 +124,19 @@ namespace RulesEngine /// CompiledRule. public IDictionary> GetCompiledRules(string compiledRulesKey) { - return _compileRules[compiledRulesKey].Item1; + return _compileRules.Get<(IDictionary> rules, long tick)>(compiledRulesKey).rules; } /// Removes the specified workflow name. /// Name of the workflow. public void Remove(string workflowName) { - if (_workflow.TryRemove(workflowName, out (Workflow, Int64) workflowObj)) + if (_workflow.TryRemove(workflowName, out var workflowObj)) { - var compiledKeysToRemove = _compileRules.Keys.Where(key => key.StartsWith(workflowName)); + var compiledKeysToRemove = _compileRules.GetKeys().Where(key => key.StartsWith(workflowName)); foreach (var key in compiledKeysToRemove) { - _compileRules.TryRemove(key, out (IDictionary>, Int64) val); + _compileRules.Remove(key); } } } diff --git a/src/RulesEngine/RulesEngine.cs b/src/RulesEngine/RulesEngine.cs index c45ee84..4a896d4 100644 --- a/src/RulesEngine/RulesEngine.cs +++ b/src/RulesEngine/RulesEngine.cs @@ -1,431 +1,427 @@ -// 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.Actions; -using RulesEngine.Exceptions; -using RulesEngine.ExpressionBuilders; -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 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 - public RulesEngine(string[] jsonConfig, ILogger logger = null, ReSettings reSettings = null) : this(logger, reSettings) - { - var workflow = jsonConfig.Select(item => JsonConvert.DeserializeObject(item)).ToArray(); - AddWorkflow(workflow); - } - - public RulesEngine(Workflow[] Workflows, ILogger logger = null, ReSettings reSettings = null) : this(logger, reSettings) - { - AddWorkflow(Workflows); - } - - public RulesEngine(ILogger logger = null, ReSettings reSettings = null) - { - _logger = logger ?? new NullLogger(); - _reSettings = reSettings ?? new ReSettings(); - _ruleExpressionParser = new RuleExpressionParser(_reSettings); - _ruleCompiler = new RuleCompiler(new RuleExpressionBuilderFactory(_reSettings, _ruleExpressionParser),_reSettings, _logger); - _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) - { - _logger.LogTrace($"Called {nameof(ExecuteAllRulesAsync)} for workflow {workflowName} and count of input {inputs.Count()}"); - - 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 ruleResultList = ValidateWorkflowAndExecuteRule(workflowName, ruleParams); - 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(); +// 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.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); } - /// - /// 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 - { - _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; - } - - /// - /// 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>(); - foreach (var rule in workflow.Rules.Where(c => c.Enabled)) - { - dictFunc.Add(rule.RuleName, CompileRule(rule, ruleParams, workflow.GlobalParams?.ToArray())); - } - - _rulesCache.AddOrUpdateCompiledRule(compileRulesKey, dictFunc); - _logger.LogTrace($"Rules has been compiled for the {workflowName} workflow and added to dictionary"); - 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}`"); - } - return CompileRule(currentRule, ruleParameters, workflow.GlobalParams?.ToArray()); - } - - private RuleFunc CompileRule(Rule rule, RuleParameter[] ruleParams, ScopedParam[] scopedParams) - { - return _ruleCompiler.CompileRule(rule, ruleParams, scopedParams); - } - - - - /// - /// This will execute the compiled rules - /// - /// - /// - /// list of rule result set - private List ExecuteAllRuleByWorkflow(string workflowName, RuleParameter[] ruleParameters) - { - _logger.LogTrace($"Compiled rules found for {workflowName} workflow and executed"); - - 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 key = $"{workflowName}-" + string.Join("-", ruleParams.Select(c => c.Type.Name)); - 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 - } -} + public RulesEngine(Workflow[] Workflows, ReSettings reSettings = null) : this(reSettings) + { + AddWorkflow(Workflows); + } + + public RulesEngine(ReSettings reSettings = null) + { + _reSettings = reSettings ?? new 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 ruleResultList = ValidateWorkflowAndExecuteRule(workflowName, ruleParams); + 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>(); + foreach (var rule in workflow.Rules.Where(c => c.Enabled)) + { + dictFunc.Add(rule.RuleName, CompileRule(rule, ruleParams, workflow.GlobalParams?.ToArray())); + } + + _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}`"); + } + return CompileRule(currentRule, ruleParameters, workflow.GlobalParams?.ToArray()); + } + + private RuleFunc CompileRule(Rule rule, RuleParameter[] ruleParams, ScopedParam[] scopedParams) + { + return _ruleCompiler.CompileRule(rule, 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 key = $"{workflowName}-" + string.Join("-", ruleParams.Select(c => c.Type.Name)); + 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 + } +} diff --git a/src/RulesEngine/RulesEngine.csproj b/src/RulesEngine/RulesEngine.csproj index 343bac6..c087fd3 100644 --- a/src/RulesEngine/RulesEngine.csproj +++ b/src/RulesEngine/RulesEngine.csproj @@ -31,15 +31,13 @@ - - + + - - - - + + diff --git a/test/RulesEngine.UnitTest/ActionTests/CustomActionTest.cs b/test/RulesEngine.UnitTest/ActionTests/CustomActionTest.cs index 797ce1e..2fd9767 100644 --- a/test/RulesEngine.UnitTest/ActionTests/CustomActionTest.cs +++ b/test/RulesEngine.UnitTest/ActionTests/CustomActionTest.cs @@ -19,7 +19,7 @@ namespace RulesEngine.UnitTest.ActionTests public async Task CustomActionOnRuleMustHaveContextValues() { var workflow = GetWorkflow(); - var re = new RulesEngine(workflow, null, reSettings: new ReSettings { + var re = new RulesEngine(workflow, reSettings: new ReSettings { CustomActions = new Dictionary> { { "ReturnContext", () => new ReturnContextAction() } @@ -39,7 +39,7 @@ namespace RulesEngine.UnitTest.ActionTests var workflowViaTextJson = System.Text.Json.JsonSerializer.Deserialize(workflowStr,serializationOptions); - var re = new RulesEngine(workflow, null, reSettings: new ReSettings { + var re = new RulesEngine(workflow, reSettings: new ReSettings { CustomActions = new Dictionary> { { "ReturnContext", () => new ReturnContextAction() } diff --git a/test/RulesEngine.UnitTest/ActionTests/RulesEngineWithActionsTests.cs b/test/RulesEngine.UnitTest/ActionTests/RulesEngineWithActionsTests.cs index 942959e..cb0e99b 100644 --- a/test/RulesEngine.UnitTest/ActionTests/RulesEngineWithActionsTests.cs +++ b/test/RulesEngine.UnitTest/ActionTests/RulesEngineWithActionsTests.cs @@ -23,6 +23,16 @@ namespace RulesEngine.UnitTest Assert.Equal(2 * 2, result.Output); } + [Fact] + public async Task WhenExpressionIsSuccess_ComplexOutputExpressionAction_ReturnsExpressionEvaluation() + { + var engine = new RulesEngine(GetWorkflowWithActions()); + var result = await engine.ExecuteActionWorkflowAsync("ActionWorkflow", "ComplexOutputRuleTest", new RuleParameter[0]); + Assert.NotNull(result); + dynamic output = result.Output; + Assert.Equal(2, output.test); + } + [Fact] public async Task WhenExpressionIsSuccess_EvaluateRuleAction_ReturnsExpressionEvaluation() { @@ -108,6 +118,19 @@ namespace RulesEngine.UnitTest } } }, + new Rule{ + RuleName = "ComplexOutputRuleTest", + RuleExpressionType = RuleExpressionType.LambdaExpression, + Expression = "1 == 1", + Actions = new RuleActions{ + OnSuccess = new ActionInfo{ + Name = "OutputExpression", + Context = new Dictionary{ + {"expression", "new (2 as test)"} + } + } + } + }, new Rule{ RuleName = "EvaluateRuleTest", RuleExpressionType = RuleExpressionType.LambdaExpression, diff --git a/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs index 209c6ba..a4a6afb 100644 --- a/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs +++ b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs @@ -1,867 +1,885 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.Extensions.Logging; -using Moq; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using RulesEngine.Exceptions; -using RulesEngine.HelperFunctions; -using RulesEngine.Interfaces; -using RulesEngine.Models; -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Dynamic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using Xunit; - -namespace RulesEngine.UnitTest -{ - [Trait("Category", "Unit")] - [ExcludeFromCodeCoverage] - public class RulesEngineTest - { - [Theory] - [InlineData("rules1.json")] - public void RulesEngine_New_ReturnsNotNull(string ruleFileName) - { - var re = GetRulesEngine(ruleFileName); - Assert.NotNull(re); - } - - [Theory] - [InlineData("rules2.json")] - public async Task RulesEngine_InjectedRules_ContainsInjectedRules(string ruleFileName) - { - var re = GetRulesEngine(ruleFileName); - - dynamic input1 = GetInput1(); - dynamic input2 = GetInput2(); - dynamic input3 = GetInput3(); - - List result = await re.ExecuteAllRulesAsync("inputWorkflowReference", input1, input2, input3); - Assert.NotNull(result); - Assert.True(result.Any()); - } - - [Theory] - [InlineData("rules2.json")] - public async Task ExecuteRule_ReturnsListOfRuleResultTree(string ruleFileName) - { - var re = GetRulesEngine(ruleFileName); - - dynamic input1 = GetInput1(); - dynamic input2 = GetInput2(); - dynamic input3 = GetInput3(); - - List result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); - Assert.NotNull(result); - Assert.IsType>(result); - Assert.Contains(result, c => c.IsSuccess); - } - - [Theory] - [InlineData("rules1.json", "rules6.json")] - public async Task ExecuteRule_AddWorkflowWithSameName_ThrowsValidationException(string previousWorkflowFile, string newWorkflowFile) - { - var re = GetRulesEngine(previousWorkflowFile); - - dynamic input1 = GetInput1(); - dynamic input2 = GetInput2(); - dynamic input3 = GetInput3(); - - // Run previous rules. - List result1 = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); - Assert.NotNull(result1); - Assert.IsType>(result1); - Assert.Contains(result1, c => c.IsSuccess); - - // Fetch and add new rules. - var newWorkflow = ParseAsWorkflow(newWorkflowFile); - - Assert.Throws(() => re.AddWorkflow(newWorkflow)); - } - - [Theory] - [InlineData("rules1.json", "rules6.json")] - public async Task ExecuteRule_AddOrUpdateWorkflow_ExecutesUpdatedRules(string previousWorkflowFile, string newWorkflowFile) - { - var re = GetRulesEngine(previousWorkflowFile); - - dynamic input1 = GetInput1(); - dynamic input2 = GetInput2(); - dynamic input3 = GetInput3(); - - // Run previous rules. - List result1 = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); - Assert.NotNull(result1); - Assert.IsType>(result1); - Assert.Contains(result1, c => c.IsSuccess); - - // Fetch and update new rules. - Workflow newWorkflow = ParseAsWorkflow(newWorkflowFile); - re.AddOrUpdateWorkflow(newWorkflow); - - // Run new rules. - List result2 = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); - Assert.NotNull(result2); - Assert.IsType>(result2); - Assert.DoesNotContain(result2, c => c.IsSuccess); - - // New execution should have different result than previous execution. - var previousResults = result1.Select(c => new { c.Rule.RuleName, c.IsSuccess }); - var newResults = result2.Select(c => new { c.Rule.RuleName, c.IsSuccess }); - Assert.NotEqual(previousResults, newResults); - } - - [Theory] - [InlineData("rules2.json")] - public void GetAllRegisteredWorkflows_ReturnsListOfAllWorkflows(string ruleFileName) - { - var re = GetRulesEngine(ruleFileName); - var workflow = re.GetAllRegisteredWorkflowNames(); - - Assert.NotNull(workflow); - Assert.Equal(2, workflow.Count); - Assert.Contains("inputWorkflow", workflow); - } - - [Fact] - public void GetAllRegisteredWorkflows_NoWorkflow_ReturnsEmptyList() - { - var re = new RulesEngine(); - var workflow = re.GetAllRegisteredWorkflowNames(); - - Assert.NotNull(workflow); - Assert.Empty(workflow); - } - - [Theory] - [InlineData("rules2.json")] - public async Task ExecuteRule_ManyInputs_ReturnsListOfRuleResultTree(string ruleFileName) - { - var re = GetRulesEngine(ruleFileName); - - dynamic input1 = GetInput1(); - dynamic input2 = GetInput2(); - dynamic input3 = GetInput3(); - - dynamic input4 = GetInput1(); - dynamic input5 = GetInput2(); - dynamic input6 = GetInput3(); - - dynamic input7 = GetInput1(); - dynamic input8 = GetInput2(); - dynamic input9 = GetInput3(); - - dynamic input10 = GetInput1(); - dynamic input11 = GetInput2(); - dynamic input12 = GetInput3(); - - dynamic input13 = GetInput1(); - dynamic input14 = GetInput2(); - dynamic input15 = GetInput3(); - - - dynamic input16 = GetInput1(); - dynamic input17 = GetInput2(); - dynamic input18 = GetInput3(); - - List result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3, input4, input5, input6, input7, input8, input9, input10, input11, input12, input13, input14, input15, input16, input17, input18); - Assert.NotNull(result); - Assert.IsType>(result); - Assert.Contains(result, c => c.IsSuccess); - } - - - [Theory] - [InlineData("rules2.json")] - public async Task ExecuteRule_CalledMultipleTimes_ReturnsSameResult(string ruleFileName) - { - var re = GetRulesEngine(ruleFileName); - - dynamic input1 = GetInput1(); - dynamic input2 = GetInput2(); - dynamic input3 = GetInput3(); - - List result1 = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); - Assert.NotNull(result1); - Assert.IsType>(result1); - Assert.Contains(result1, c => c.IsSuccess); - - List result2 = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); - Assert.NotNull(result2); - Assert.IsType>(result2); - Assert.Contains(result2, c => c.IsSuccess); - - var expected = result1.Select(c => new { c.Rule.RuleName, c.IsSuccess }); - var actual = result2.Select(c => new { c.Rule.RuleName, c.IsSuccess }); - Assert.Equal(expected, actual); - - - } - - [Theory] - [InlineData("rules2.json")] - public async Task ExecuteRule_SingleObject_ReturnsListOfRuleResultTree(string ruleFileName) - { - var re = GetRulesEngine(ruleFileName); - - dynamic input1 = GetInput1(); - dynamic input2 = GetInput2(); - dynamic input3 = GetInput3(); - - List result = await re.ExecuteAllRulesAsync("inputWorkflow", input1); - Assert.NotNull(result); - Assert.IsType>(result); - Assert.DoesNotContain(result, c => c.IsSuccess); - } - - [Theory] - [InlineData("rules3.json")] - public async Task ExecuteRule_ExceptionScenario_RulesInvalid(string ruleFileName) - { - var re = GetRulesEngine(ruleFileName); - - dynamic input1 = GetInput1(); - dynamic input2 = GetInput2(); - dynamic input3 = GetInput3(); - - List result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); - Assert.NotNull(result); - Assert.False(string.IsNullOrEmpty(result[0].ExceptionMessage) || string.IsNullOrWhiteSpace(result[0].ExceptionMessage)); - } - - [Theory] - [InlineData("rules2.json")] - [Obsolete] - public async Task ExecuteRule_ReturnsListOfRuleResultTree_ResultMessage(string ruleFileName) - { - var re = GetRulesEngine(ruleFileName); - - dynamic input1 = GetInput1(); - dynamic input2 = GetInput2(); - dynamic input3 = GetInput3(); - - List result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); - Assert.NotNull(result); - Assert.NotNull(result.First().GetMessages()); - Assert.NotNull(result.First().GetMessages().WarningMessages); - } - - [Fact] - public void RulesEngine_New_IncorrectJSON_ThrowsException() - { - Assert.Throws(() => { - var workflow = new Workflow(); - var re = CreateRulesEngine(workflow); - }); - - Assert.Throws(() => { - var workflow = new Workflow() { WorkflowName = "test" }; - var re = CreateRulesEngine(workflow); - }); - } - - - [Theory] - [InlineData("rules1.json")] - public async Task ExecuteRule_InvalidWorkFlow_ThrowsException(string ruleFileName) - { - var re = GetRulesEngine(ruleFileName); - dynamic input = GetInput1(); - - await Assert.ThrowsAsync(async () => { await re.ExecuteAllRulesAsync("inputWorkflow1", input); }); - } - - [Theory] - [InlineData("rules1.json")] - public async Task RemoveWorkflow_RemovesWorkflow(string ruleFileName) - { - var re = GetRulesEngine(ruleFileName); - dynamic input1 = GetInput1(); - dynamic input2 = GetInput2(); - dynamic input3 = GetInput3(); - - var result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); - Assert.NotNull(result); - re.RemoveWorkflow("inputWorkflow"); - - await Assert.ThrowsAsync(async () => await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3)); - } - - - [Theory] - [InlineData("rules1.json")] - public async Task ClearWorkflow_RemovesAllWorkflow(string ruleFileName) - { - var re = GetRulesEngine(ruleFileName); - re.ClearWorkflows(); - - dynamic input1 = GetInput1(); - dynamic input2 = GetInput2(); - dynamic input3 = GetInput3(); - - await Assert.ThrowsAsync(async () => await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3)); - await Assert.ThrowsAsync(async () => await re.ExecuteAllRulesAsync("inputWorkflowReference", input1, input2, input3)); - } - - [Theory] - [InlineData("rules1.json")] - [InlineData("rules2.json")] - public async Task ExecuteRule_InputWithVariableProps_ReturnsResult(string ruleFileName) - { - var re = GetRulesEngine(ruleFileName); - - dynamic input1 = GetInput1(); - dynamic input2 = GetInput2(); - dynamic input3 = GetInput3(); - - List result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); - Assert.NotNull(result); - Assert.IsType>(result); - Assert.Contains(result, c => c.IsSuccess); - - input3.hello = "world"; - - result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); - Assert.NotNull(result); - Assert.IsType>(result); - Assert.Contains(result, c => c.IsSuccess); - } - - [Theory] - [InlineData("rules4.json")] - public async Task RulesEngine_Execute_Rule_For_Nested_Rule_Params_Returns_Success(string ruleFileName) - { - var inputs = GetInputs4(); - - var ruleParams = new List(); - - for (var 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 = await bre.ExecuteAllRulesAsync("inputWorkflow", ruleParams?.ToArray()); - var ruleResult = result?.FirstOrDefault(r => string.Equals(r.Rule.RuleName, "GiveDiscount10", StringComparison.OrdinalIgnoreCase)); - Assert.True(ruleResult.IsSuccess); - } - - [Theory] - [InlineData("rules2.json")] - public async Task ExecuteRule_ReturnsProperErrorOnMissingRuleParameter(string ruleFileName) - { - var re = GetRulesEngine(ruleFileName); - - var input1 = new RuleParameter("customName", GetInput1()); - var input2 = new RuleParameter("input2", GetInput2()); - var input3 = new RuleParameter("input3", GetInput3()); - - var result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); - Assert.NotNull(result); - Assert.IsType>(result); - Assert.Contains(result.First().ChildResults, c => c.ExceptionMessage.Contains("Unknown identifier 'input1'")); - } - - [Theory] - [InlineData("rules5.json", "hello", true)] - [InlineData("rules5.json", null, false)] - public async Task ExecuteRule_WithInjectedUtils_ReturnsListOfRuleResultTree(string ruleFileName, string propValue, bool expectedResult) - { - var re = GetRulesEngine(ruleFileName); - - dynamic input1 = new ExpandoObject(); - if (propValue != null) - { - input1.Property1 = propValue; - } - - if (propValue == null) - { - input1.Property1 = null; - } - - var utils = new TestInstanceUtils(); - - var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1), new RuleParameter("utils", utils)); - Assert.NotNull(result); - Assert.IsType>(result); - Assert.All(result, c => Assert.Equal(expectedResult, c.IsSuccess)); - } - - [Theory] - [InlineData("rules6.json")] - public async Task ExecuteRule_RuleWithMethodExpression_ReturnsSucess(string ruleFileName) - { - var re = GetRulesEngine(ruleFileName); - - Func func = () => true; - - dynamic input1 = new ExpandoObject(); - input1.Property1 = "hello"; - input1.Boolean = false; - input1.Method = func; - - var utils = new TestInstanceUtils(); - - var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); - Assert.NotNull(result); - Assert.IsType>(result); - Assert.All(result, c => Assert.True(c.IsSuccess)); - } - - [Theory] - [InlineData("rules7.json")] - public async Task ExecuteRule_RuleWithUnaryExpression_ReturnsSucess(string ruleFileName) - { - var re = GetRulesEngine(ruleFileName); - - dynamic input1 = new ExpandoObject(); - input1.Boolean = false; - - var utils = new TestInstanceUtils(); - - var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); - Assert.NotNull(result); - Assert.IsType>(result); - Assert.All(result, c => Assert.True(c.IsSuccess)); - } - - [Theory] - [InlineData("rules8.json")] - public async Task ExecuteRule_RuleWithMemberAccessExpression_ReturnsSucess(string ruleFileName) - { - var re = GetRulesEngine(ruleFileName); - - dynamic input1 = new ExpandoObject(); - input1.Boolean = false; - - var utils = new TestInstanceUtils(); - - var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); - Assert.NotNull(result); - Assert.IsType>(result); - Assert.All(result, c => Assert.False(c.IsSuccess)); - } - - [Theory] - [InlineData("rules9.json")] - public async Task ExecuteRule_MissingMethodInExpression_ReturnsException(string ruleFileName) - { - var re = GetRulesEngine(ruleFileName, new ReSettings() { EnableExceptionAsErrorMessage = false }); - - dynamic input1 = new ExpandoObject(); - input1.Data = new { TestProperty = "" }; - input1.Boolean = false; - - var utils = new TestInstanceUtils(); - - await Assert.ThrowsAsync(async () => { - var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); - }); - } - - [Theory] - [InlineData("rules9.json")] - public async Task ExecuteRule_CompilationException_ReturnsAsErrorMessage(string ruleFileName) - { - var re = GetRulesEngine(ruleFileName, new ReSettings() { EnableExceptionAsErrorMessage = true }); - - dynamic input1 = new ExpandoObject(); - input1.Data = new { TestProperty = "" }; - input1.Boolean = false; - - var utils = new TestInstanceUtils(); - var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); - - Assert.NotNull(result); - Assert.StartsWith("Exception while parsing expression", result[1].ExceptionMessage); - } - - [Theory] - [InlineData("rules9.json")] - public async Task ExecuteRuleWithIgnoreException_CompilationException_DoesNotReturnsAsErrorMessage(string ruleFileName) - { - var re = GetRulesEngine(ruleFileName, new ReSettings() { EnableExceptionAsErrorMessage = true , IgnoreException = true}); - - dynamic input1 = new ExpandoObject(); - input1.Data = new { TestProperty = "" }; - input1.Boolean = false; - - var utils = new TestInstanceUtils(); - var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); - - Assert.NotNull(result); - Assert.False(result[1].ExceptionMessage.StartsWith("Exception while parsing expression")); - } - - - [Fact] - public async Task ExecuteRule_RuntimeError_ShouldReturnAsErrorMessage() - { - - var workflow = new Workflow { - WorkflowName = "TestWorkflow", - Rules = new[] { - new Rule { - RuleName = "ruleWithRuntimeError", - Expression = "input1.Country.ToLower() == \"india\"" - } - } - }; - - var re = new RulesEngine(new[] { workflow }, null, null); - var input = new RuleTestClass { - Country = null - }; - - var result = await re.ExecuteAllRulesAsync("TestWorkflow", input); - - Assert.NotNull(result); - Assert.All(result, rule => Assert.False(rule.IsSuccess)); - Assert.All(result, rule => Assert.StartsWith("Error while executing rule :", rule.ExceptionMessage)); - } - - - [Fact] - public async Task ExecuteRule_RuntimeError_ThrowsException() - { - - var workflow = new Workflow { - WorkflowName = "TestWorkflow", - Rules = new[] { - new Rule { - RuleName = "ruleWithRuntimeError", - Expression = "input1.Country.ToLower() == \"india\"" - } - } - }; - - var re = new RulesEngine(new[] { workflow }, null, new ReSettings { - EnableExceptionAsErrorMessage = false - }); - var input = new RuleTestClass { - Country = null - }; - - _ = await Assert.ThrowsAsync(async () => await re.ExecuteAllRulesAsync("TestWorkflow", input)); - - } - - [Fact] - public async Task ExecuteRule_RuntimeError_IgnoreException_DoesNotReturnException() - { - - var workflow = new Workflow { - WorkflowName = "TestWorkflow", - Rules = new[] { - new Rule { - RuleName = "ruleWithRuntimeError", - Expression = "input1.Country.ToLower() == \"india\"" - } - } - }; - - var re = new RulesEngine(new[] { workflow }, null, new ReSettings { - IgnoreException = true - }); - var input = new RuleTestClass { - Country = null - }; - - var result = await re.ExecuteAllRulesAsync("TestWorkflow", input); - - Assert.NotNull(result); - Assert.All(result, rule => Assert.False(rule.IsSuccess)); - Assert.All(result, rule => Assert.Empty(rule.ExceptionMessage)); - } - - - [Fact] - public async Task RemoveWorkFlow_ShouldRemoveAllCompiledCache() - { - var workflow = new Workflow { - WorkflowName = "Test", - Rules = new Rule[]{ - new Rule { - RuleName = "RuleWithLocalParam", - LocalParams = new List { - new LocalParam { - Name = "lp1", - Expression = "true" - } - }, - RuleExpressionType = RuleExpressionType.LambdaExpression, - Expression = "lp1 == true" - } - } - }; - - var re = new RulesEngine(); - re.AddWorkflow(workflow); - - var result1 = await re.ExecuteAllRulesAsync("Test","hello"); - Assert.True(result1.All(c => c.IsSuccess)); - - re.RemoveWorkflow("Test"); - workflow.Rules.First().LocalParams.First().Expression = "false"; - - re.AddWorkflow(workflow); - var result2 = await re.ExecuteAllRulesAsync("Test", "hello"); - Assert.True(result2.All(c => c.IsSuccess == false)); - } - - [Fact] - public async Task ClearWorkFlow_ShouldRemoveAllCompiledCache() - { - var workflow = new Workflow { - WorkflowName = "Test", - Rules = new Rule[]{ - new Rule { - RuleName = "RuleWithLocalParam", - LocalParams = new LocalParam[] { - new LocalParam { - Name = "lp1", - Expression = "true" - } - }, - RuleExpressionType = RuleExpressionType.LambdaExpression, - Expression = "lp1 == true" - } - } - }; - - var re = new RulesEngine(); - re.AddWorkflow(workflow); - - var result1 = await re.ExecuteAllRulesAsync("Test", "hello"); - Assert.True(result1.All(c => c.IsSuccess)); - - re.ClearWorkflows(); - workflow.Rules.First().LocalParams.First().Expression = "false"; - - re.AddWorkflow(workflow); - var result2 = await re.ExecuteAllRulesAsync("Test", "hello"); - Assert.True(result2.All(c => c.IsSuccess == false)); - } - - [Fact] - public async Task ExecuteRule_WithNullInput_ShouldNotThrowException() - { - var workflow = new Workflow { - WorkflowName = "Test", - Rules = new Rule[]{ - new Rule { - RuleName = "RuleWithLocalParam", - - RuleExpressionType = RuleExpressionType.LambdaExpression, - Expression = "input1 == null || input1.hello.world = \"wow\"" - } - } - }; - - var re = new RulesEngine(); - re.AddWorkflow(workflow); - - var result1 = await re.ExecuteAllRulesAsync("Test", new RuleParameter("input1", value:null)); - Assert.True(result1.All(c => c.IsSuccess)); - - - var result2 = await re.ExecuteAllRulesAsync("Test",new object[] { null }); - Assert.True(result2.All(c => c.IsSuccess)); - - dynamic input1 = new ExpandoObject(); - input1.hello = new ExpandoObject(); - input1.hello.world = "wow"; - - List result3 = await re.ExecuteAllRulesAsync("Test", input1); - Assert.True(result3.All(c => c.IsSuccess)); - - } - - [Fact] - public async Task ExecuteRule_SpecialCharInWorkflowName_RunsSuccessfully() - { - var workflow = new Workflow { - WorkflowName = "Exámple", - Rules = new Rule[]{ - new Rule { - RuleName = "RuleWithLocalParam", - - RuleExpressionType = RuleExpressionType.LambdaExpression, - Expression = "input1 == null || input1.hello.world = \"wow\"" - } - } - }; - - var workflowStr = "{\"WorkflowName\":\"Exámple\",\"WorkflowsToInject\":null,\"GlobalParams\":null,\"Rules\":[{\"RuleName\":\"RuleWithLocalParam\",\"Properties\":null,\"Operator\":null,\"ErrorMessage\":null,\"Enabled\":true,\"ErrorType\":\"Warning\",\"RuleExpressionType\":\"LambdaExpression\",\"WorkflowsToInject\":null,\"Rules\":null,\"LocalParams\":null,\"Expression\":\"input1 == null || input1.hello.world = \\\"wow\\\"\",\"Actions\":null,\"SuccessEvent\":null}]}"; - - var re = new RulesEngine(new string[] { workflowStr },null,null); - - dynamic input1 = new ExpandoObject(); - input1.hello = new ExpandoObject(); - input1.hello.world = "wow"; - - List result3 = await re.ExecuteAllRulesAsync("Exámple", input1); - Assert.True(result3.All(c => c.IsSuccess)); - +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using RulesEngine.Exceptions; +using RulesEngine.HelperFunctions; +using RulesEngine.Interfaces; +using RulesEngine.Models; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Dynamic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Threading.Tasks; +using Xunit; + +namespace RulesEngine.UnitTest +{ + [Trait("Category", "Unit")] + [ExcludeFromCodeCoverage] + public class RulesEngineTest + { + [Theory] + [InlineData("rules1.json")] + public void RulesEngine_New_ReturnsNotNull(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + Assert.NotNull(re); } - [Fact] - public void ContainsWorkFlowName_ShouldReturn() - { - const string ExistedWorkflowName = "ExistedWorkflowName"; - const string NotExistedWorkflowName = "NotExistedWorkflowName"; - - var workflow = new Workflow { - WorkflowName = ExistedWorkflowName, - Rules = new Rule[]{ - new Rule { - RuleName = "Rule", - RuleExpressionType = RuleExpressionType.LambdaExpression, - Expression = "1==1" - } - } - }; - - var re = new RulesEngine(); - re.AddWorkflow(workflow); - - Assert.True(re.ContainsWorkflow(ExistedWorkflowName)); - Assert.False(re.ContainsWorkflow(NotExistedWorkflowName)); - } - - [Theory] - [InlineData(typeof(RulesEngine),typeof(IRulesEngine))] - public void Class_PublicMethods_ArePartOfInterface(Type classType, Type interfaceType) - { - var classMethods = classType.GetMethods(BindingFlags.DeclaredOnly | - BindingFlags.Public | - BindingFlags.Instance); - - - var interfaceMethods = interfaceType.GetMethods(); - - - Assert.Equal(interfaceMethods.Count(), classMethods.Count()); - } - - - - private RulesEngine CreateRulesEngine(Workflow workflow) - { - var json = JsonConvert.SerializeObject(workflow); - return new RulesEngine(new string[] { json }, null); - } - - private RulesEngine GetRulesEngine(string filename, ReSettings reSettings = null) - { - var data = GetFileContent(filename); - - var injectWorkflow = new Workflow { - WorkflowName = "inputWorkflowReference", - WorkflowsToInject = new List { "inputWorkflow" } - }; - - var injectWorkflowStr = JsonConvert.SerializeObject(injectWorkflow); - var mockLogger = new Mock(); - return new RulesEngine(new string[] { data, injectWorkflowStr }, mockLogger.Object, reSettings); - } - - private string GetFileContent(string filename) - { - var filePath = Path.Combine(Directory.GetCurrentDirectory() as string, "TestData", filename); - return File.ReadAllText(filePath); - } - - private Workflow ParseAsWorkflow(string WorkflowsFileName) - { - string content = GetFileContent(WorkflowsFileName); - return JsonConvert.DeserializeObject(content); - } - - private dynamic GetInput1() - { - var converter = new ExpandoObjectConverter(); - var basicInfo = "{\"name\": \"Dishant\",\"email\": \"abc@xyz.com\",\"creditHistory\": \"good\",\"country\": \"canada\",\"loyaltyFactor\": 3,\"totalPurchasesToDate\": 10000}"; - return JsonConvert.DeserializeObject(basicInfo, converter); - } - - private dynamic GetInput2() - { - var converter = new ExpandoObjectConverter(); - var orderInfo = "{\"totalOrders\": 5,\"recurringItems\": 2}"; - return JsonConvert.DeserializeObject(orderInfo, converter); - } - - private dynamic GetInput3() - { - var converter = new ExpandoObjectConverter(); - 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\": \"abc@xyz.com\",\"creditHistory\": \"good\",\"country\": \"canada\",\"loyaltyFactor\": 3,\"totalPurchasesToDate\": 70000}"; - var orderInfo = "{\"totalOrders\": 50,\"recurringItems\": 2}"; - var telemetryInfo = "{\"noOfVisitsPerMonth\": 10,\"percentageOfBuyingToVisit\": 15}"; - var laborCategoriesInput = "[{\"country\": \"india\", \"loyaltyFactor\": 2, \"totalPurchasesToDate\": 20000}]"; - var currentLaborCategoryInput = "{\"CurrentLaborCategoryProp\":\"TestVal2\"}"; - - dynamic input1 = JsonConvert.DeserializeObject>(laborCategoriesInput); - dynamic input2 = JsonConvert.DeserializeObject(currentLaborCategoryInput); - dynamic input3 = JsonConvert.DeserializeObject(telemetryInfo); - dynamic input4 = JsonConvert.DeserializeObject(basicInfo); - dynamic input5 = JsonConvert.DeserializeObject(orderInfo); - - var inputs = new dynamic[] - { - input1, - input2, - input3, - input4, - input5 - }; - - return inputs; - } - - [ExcludeFromCodeCoverage] - private class TestInstanceUtils - { - public bool CheckExists(string str) - { - if (!string.IsNullOrEmpty(str)) - { - return true; - } - - return false; - } - - } - - } + [Theory] + [InlineData("rules2.json")] + public async Task RulesEngine_InjectedRules_ContainsInjectedRules(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + + dynamic input1 = GetInput1(); + dynamic input2 = GetInput2(); + dynamic input3 = GetInput3(); + + List result = await re.ExecuteAllRulesAsync("inputWorkflowReference", input1, input2, input3); + Assert.NotNull(result); + Assert.True(result.Any()); + } + + [Theory] + [InlineData("rules2.json")] + public async Task ExecuteRule_ReturnsListOfRuleResultTree(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + + dynamic input1 = GetInput1(); + dynamic input2 = GetInput2(); + dynamic input3 = GetInput3(); + + List result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); + Assert.NotNull(result); + Assert.IsType>(result); + Assert.Contains(result, c => c.IsSuccess); + } + + [Theory] + [InlineData("rules1.json", "rules6.json")] + public async Task ExecuteRule_AddWorkflowWithSameName_ThrowsValidationException(string previousWorkflowFile, string newWorkflowFile) + { + var re = GetRulesEngine(previousWorkflowFile); + + dynamic input1 = GetInput1(); + dynamic input2 = GetInput2(); + dynamic input3 = GetInput3(); + + // Run previous rules. + List result1 = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); + Assert.NotNull(result1); + Assert.IsType>(result1); + Assert.Contains(result1, c => c.IsSuccess); + + // Fetch and add new rules. + var newWorkflow = ParseAsWorkflow(newWorkflowFile); + + Assert.Throws(() => re.AddWorkflow(newWorkflow)); + } + + [Theory] + [InlineData("rules1.json", "rules6.json")] + public async Task ExecuteRule_AddOrUpdateWorkflow_ExecutesUpdatedRules(string previousWorkflowFile, string newWorkflowFile) + { + var re = GetRulesEngine(previousWorkflowFile); + + dynamic input1 = GetInput1(); + dynamic input2 = GetInput2(); + dynamic input3 = GetInput3(); + + // Run previous rules. + List result1 = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); + Assert.NotNull(result1); + Assert.IsType>(result1); + Assert.Contains(result1, c => c.IsSuccess); + + // Fetch and update new rules. + Workflow newWorkflow = ParseAsWorkflow(newWorkflowFile); + re.AddOrUpdateWorkflow(newWorkflow); + + // Run new rules. + List result2 = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); + Assert.NotNull(result2); + Assert.IsType>(result2); + Assert.DoesNotContain(result2, c => c.IsSuccess); + + // New execution should have different result than previous execution. + var previousResults = result1.Select(c => new { c.Rule.RuleName, c.IsSuccess }); + var newResults = result2.Select(c => new { c.Rule.RuleName, c.IsSuccess }); + Assert.NotEqual(previousResults, newResults); + } + + [Theory] + [InlineData("rules2.json")] + public void GetAllRegisteredWorkflows_ReturnsListOfAllWorkflows(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + var workflow = re.GetAllRegisteredWorkflowNames(); + + Assert.NotNull(workflow); + Assert.Equal(2, workflow.Count); + Assert.Contains("inputWorkflow", workflow); + } + + [Fact] + public void GetAllRegisteredWorkflows_NoWorkflow_ReturnsEmptyList() + { + var re = new RulesEngine(); + var workflow = re.GetAllRegisteredWorkflowNames(); + + Assert.NotNull(workflow); + Assert.Empty(workflow); + } + + [Theory] + [InlineData("rules2.json")] + public async Task ExecuteRule_ManyInputs_ReturnsListOfRuleResultTree(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + + dynamic input1 = GetInput1(); + dynamic input2 = GetInput2(); + dynamic input3 = GetInput3(); + + dynamic input4 = GetInput1(); + dynamic input5 = GetInput2(); + dynamic input6 = GetInput3(); + + dynamic input7 = GetInput1(); + dynamic input8 = GetInput2(); + dynamic input9 = GetInput3(); + + dynamic input10 = GetInput1(); + dynamic input11 = GetInput2(); + dynamic input12 = GetInput3(); + + dynamic input13 = GetInput1(); + dynamic input14 = GetInput2(); + dynamic input15 = GetInput3(); + + + dynamic input16 = GetInput1(); + dynamic input17 = GetInput2(); + dynamic input18 = GetInput3(); + + List result = await re.ExecuteAllRulesAsync("inputWorkflow", + input1, input2, input3, input4, input5, input6, input7, input8, input9, input10, input11, input12, input13, input14, input15, input16, input17, input18); + //, input9, input10, input11, input12, input13, input14, input15, input16, input17, input18); + Assert.NotNull(result); + Assert.IsType>(result); + Assert.Contains(result, c => c.IsSuccess); + } + + + [Theory] + [InlineData("rules2.json")] + public async Task ExecuteRule_CalledMultipleTimes_ReturnsSameResult(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + + dynamic input1 = GetInput1(); + dynamic input2 = GetInput2(); + dynamic input3 = GetInput3(); + + List result1 = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); + Assert.NotNull(result1); + Assert.IsType>(result1); + Assert.Contains(result1, c => c.IsSuccess); + + List result2 = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); + Assert.NotNull(result2); + Assert.IsType>(result2); + Assert.Contains(result2, c => c.IsSuccess); + + var expected = result1.Select(c => new { c.Rule.RuleName, c.IsSuccess }); + var actual = result2.Select(c => new { c.Rule.RuleName, c.IsSuccess }); + Assert.Equal(expected, actual); + + + } + + [Theory] + [InlineData("rules2.json")] + public async Task ExecuteRule_SingleObject_ReturnsListOfRuleResultTree(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + + dynamic input1 = GetInput1(); + dynamic input2 = GetInput2(); + dynamic input3 = GetInput3(); + + List result = await re.ExecuteAllRulesAsync("inputWorkflow", input1); + Assert.NotNull(result); + Assert.IsType>(result); + Assert.DoesNotContain(result, c => c.IsSuccess); + } + + [Theory] + [InlineData("rules3.json")] + public async Task ExecuteRule_ExceptionScenario_RulesInvalid(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + + dynamic input1 = GetInput1(); + dynamic input2 = GetInput2(); + dynamic input3 = GetInput3(); + + List result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); + Assert.NotNull(result); + Assert.False(string.IsNullOrEmpty(result[0].ExceptionMessage) || string.IsNullOrWhiteSpace(result[0].ExceptionMessage)); + } + + [Theory] + [InlineData("rules2.json")] + [Obsolete] + public async Task ExecuteRule_ReturnsListOfRuleResultTree_ResultMessage(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + + dynamic input1 = GetInput1(); + dynamic input2 = GetInput2(); + dynamic input3 = GetInput3(); + + List result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); + Assert.NotNull(result); + Assert.NotNull(result.First().GetMessages()); + Assert.NotNull(result.First().GetMessages().WarningMessages); + } + + [Fact] + public void RulesEngine_New_IncorrectJSON_ThrowsException() + { + Assert.Throws(() => { + var workflow = new Workflow(); + var re = CreateRulesEngine(workflow); + }); + + Assert.Throws(() => { + var workflow = new Workflow() { WorkflowName = "test" }; + var re = CreateRulesEngine(workflow); + }); + } + + + [Theory] + [InlineData("rules1.json")] + public async Task ExecuteRule_InvalidWorkFlow_ThrowsException(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + dynamic input = GetInput1(); + + await Assert.ThrowsAsync(async () => { await re.ExecuteAllRulesAsync("inputWorkflow1", input); }); + } + + [Theory] + [InlineData("rules1.json")] + public async Task RemoveWorkflow_RemovesWorkflow(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + dynamic input1 = GetInput1(); + dynamic input2 = GetInput2(); + dynamic input3 = GetInput3(); + + var result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); + Assert.NotNull(result); + re.RemoveWorkflow("inputWorkflow"); + + await Assert.ThrowsAsync(async () => await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3)); + } + + + [Theory] + [InlineData("rules1.json")] + public async Task ClearWorkflow_RemovesAllWorkflow(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + re.ClearWorkflows(); + + dynamic input1 = GetInput1(); + dynamic input2 = GetInput2(); + dynamic input3 = GetInput3(); + + await Assert.ThrowsAsync(async () => await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3)); + await Assert.ThrowsAsync(async () => await re.ExecuteAllRulesAsync("inputWorkflowReference", input1, input2, input3)); + } + + [Theory] + [InlineData("rules1.json")] + [InlineData("rules2.json")] + public async Task ExecuteRule_InputWithVariableProps_ReturnsResult(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + + dynamic input1 = GetInput1(); + dynamic input2 = GetInput2(); + dynamic input3 = GetInput3(); + + List result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); + Assert.NotNull(result); + Assert.IsType>(result); + Assert.Contains(result, c => c.IsSuccess); + + input3.hello = "world"; + + result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); + Assert.NotNull(result); + Assert.IsType>(result); + Assert.Contains(result, c => c.IsSuccess); + } + + [Theory] + [InlineData("rules4.json")] + public async Task RulesEngine_Execute_Rule_For_Nested_Rule_Params_Returns_Success(string ruleFileName) + { + var inputs = GetInputs4(); + + var ruleParams = new List(); + + for (var 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 = await bre.ExecuteAllRulesAsync("inputWorkflow", ruleParams?.ToArray()); + var ruleResult = result?.FirstOrDefault(r => string.Equals(r.Rule.RuleName, "GiveDiscount10", StringComparison.OrdinalIgnoreCase)); + Assert.True(ruleResult.IsSuccess); + } + + [Theory] + [InlineData("rules2.json")] + public async Task ExecuteRule_ReturnsProperErrorOnMissingRuleParameter(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + + var input1 = new RuleParameter("customName", GetInput1()); + var input2 = new RuleParameter("input2", GetInput2()); + var input3 = new RuleParameter("input3", GetInput3()); + + var result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); + Assert.NotNull(result); + Assert.IsType>(result); + Assert.Contains(result.First().ChildResults, c => c.ExceptionMessage.Contains("Unknown identifier 'input1'")); + } + + [Theory] + [InlineData("rules5.json", "hello", true)] + [InlineData("rules5.json", null, false)] + public async Task ExecuteRule_WithInjectedUtils_ReturnsListOfRuleResultTree(string ruleFileName, string propValue, bool expectedResult) + { + var re = GetRulesEngine(ruleFileName); + + dynamic input1 = new ExpandoObject(); + + input1.Property1 = propValue; + + + var utils = new TestInstanceUtils(); + + var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1), new RuleParameter("utils", utils)); + Assert.NotNull(result); + Assert.IsType>(result); + Assert.All(result, c => Assert.Equal(expectedResult, c.IsSuccess)); + } + + [Theory] + [InlineData("rules6.json")] + public async Task ExecuteRule_RuleWithMethodExpression_ReturnsSucess(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + + Func func = () => true; + + dynamic input1 = new ExpandoObject(); + input1.Property1 = "hello"; + input1.Boolean = false; + input1.Method = func; + + var utils = new TestInstanceUtils(); + + var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); + Assert.NotNull(result); + Assert.IsType>(result); + Assert.All(result, c => Assert.True(c.IsSuccess)); + } + + [Theory] + [InlineData("rules7.json")] + public async Task ExecuteRule_RuleWithUnaryExpression_ReturnsSucess(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + + dynamic input1 = new ExpandoObject(); + input1.Boolean = false; + + var utils = new TestInstanceUtils(); + + var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); + Assert.NotNull(result); + Assert.IsType>(result); + Assert.All(result, c => Assert.True(c.IsSuccess)); + } + + [Theory] + [InlineData("rules8.json")] + public async Task ExecuteRule_RuleWithMemberAccessExpression_ReturnsSucess(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + + dynamic input1 = new ExpandoObject(); + input1.Boolean = false; + + var utils = new TestInstanceUtils(); + + var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); + Assert.NotNull(result); + Assert.IsType>(result); + Assert.All(result, c => Assert.False(c.IsSuccess)); + } + + [Theory] + [InlineData("rules9.json")] + public async Task ExecuteRule_MissingMethodInExpression_ReturnsException(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName, new ReSettings() { EnableExceptionAsErrorMessage = false }); + + dynamic input1 = new ExpandoObject(); + input1.Data = new { TestProperty = "" }; + input1.Boolean = false; + + var utils = new TestInstanceUtils(); + + await Assert.ThrowsAsync(async () => { + var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); + }); + } + + [Theory] + [InlineData("rules9.json")] + public async Task ExecuteRule_CompilationException_ReturnsAsErrorMessage(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName, new ReSettings() { EnableExceptionAsErrorMessage = true }); + + dynamic input1 = new ExpandoObject(); + input1.Data = new { TestProperty = "" }; + input1.Boolean = false; + + var utils = new TestInstanceUtils(); + var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); + + Assert.NotNull(result); + Assert.StartsWith("Exception while parsing expression", result[1].ExceptionMessage); + } + + [Theory] + [InlineData("rules9.json")] + public async Task ExecuteRuleWithIgnoreException_CompilationException_DoesNotReturnsAsErrorMessage(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName, new ReSettings() { EnableExceptionAsErrorMessage = true, IgnoreException = true }); + + dynamic input1 = new ExpandoObject(); + input1.Data = new { TestProperty = "" }; + input1.Boolean = false; + + var utils = new TestInstanceUtils(); + var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); + + Assert.NotNull(result); + Assert.False(result[1].ExceptionMessage.StartsWith("Exception while parsing expression")); + } + + + [Theory] + [InlineData("rules10.json")] + public async Task ExecuteRuleWithJsonElement(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName, new ReSettings() { + EnableExceptionAsErrorMessage = true, + CustomTypes = new Type[] { typeof(System.Text.Json.JsonElement) } + + }); + + var input1 = new { + Data = System.Text.Json.JsonSerializer.SerializeToElement(new { + category= "abc" + }) + }; + + var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); + + Assert.NotNull(result); + Assert.All(result, c => Assert.True(c.IsSuccess)); + } + + + [Fact] + public async Task ExecuteRule_RuntimeError_ShouldReturnAsErrorMessage() + { + + var workflow = new Workflow { + WorkflowName = "TestWorkflow", + Rules = new[] { + new Rule { + RuleName = "ruleWithRuntimeError", + Expression = "input1.Country.ToLower() == \"india\"" + } + } + }; + + var re = new RulesEngine(new[] { workflow }, null); + var input = new RuleTestClass { + Country = null + }; + + var result = await re.ExecuteAllRulesAsync("TestWorkflow", input); + + Assert.NotNull(result); + Assert.All(result, rule => Assert.False(rule.IsSuccess)); + Assert.All(result, rule => Assert.StartsWith("Error while executing rule :", rule.ExceptionMessage)); + } + + + [Fact] + public async Task ExecuteRule_RuntimeError_ThrowsException() + { + + var workflow = new Workflow { + WorkflowName = "TestWorkflow", + Rules = new[] { + new Rule { + RuleName = "ruleWithRuntimeError", + Expression = "input1.Country.ToLower() == \"india\"" + } + } + }; + + var re = new RulesEngine(new[] { workflow }, new ReSettings { + EnableExceptionAsErrorMessage = false + }); + var input = new RuleTestClass { + Country = null + }; + + _ = await Assert.ThrowsAsync(async () => await re.ExecuteAllRulesAsync("TestWorkflow", input)); + + } + + [Fact] + public async Task ExecuteRule_RuntimeError_IgnoreException_DoesNotReturnException() + { + + var workflow = new Workflow { + WorkflowName = "TestWorkflow", + Rules = new[] { + new Rule { + RuleName = "ruleWithRuntimeError", + Expression = "input1.Country.ToLower() == \"india\"" + } + } + }; + + var re = new RulesEngine(new[] { workflow }, new ReSettings { + IgnoreException = true + }); + var input = new RuleTestClass { + Country = null + }; + + var result = await re.ExecuteAllRulesAsync("TestWorkflow", input); + + Assert.NotNull(result); + Assert.All(result, rule => Assert.False(rule.IsSuccess)); + Assert.All(result, rule => Assert.Empty(rule.ExceptionMessage)); + } + + + [Fact] + public async Task RemoveWorkFlow_ShouldRemoveAllCompiledCache() + { + var workflow = new Workflow { + WorkflowName = "Test", + Rules = new Rule[]{ + new Rule { + RuleName = "RuleWithLocalParam", + LocalParams = new List { + new LocalParam { + Name = "lp1", + Expression = "true" + } + }, + RuleExpressionType = RuleExpressionType.LambdaExpression, + Expression = "lp1 == true" + } + } + }; + + var re = new RulesEngine(); + re.AddWorkflow(workflow); + + var result1 = await re.ExecuteAllRulesAsync("Test", "hello"); + Assert.True(result1.All(c => c.IsSuccess)); + + re.RemoveWorkflow("Test"); + workflow.Rules.First().LocalParams.First().Expression = "false"; + + re.AddWorkflow(workflow); + var result2 = await re.ExecuteAllRulesAsync("Test", "hello"); + Assert.True(result2.All(c => c.IsSuccess == false)); + } + + [Fact] + public async Task ClearWorkFlow_ShouldRemoveAllCompiledCache() + { + var workflow = new Workflow { + WorkflowName = "Test", + Rules = new Rule[]{ + new Rule { + RuleName = "RuleWithLocalParam", + LocalParams = new LocalParam[] { + new LocalParam { + Name = "lp1", + Expression = "true" + } + }, + RuleExpressionType = RuleExpressionType.LambdaExpression, + Expression = "lp1 == true" + } + } + }; + + var re = new RulesEngine(); + re.AddWorkflow(workflow); + + var result1 = await re.ExecuteAllRulesAsync("Test", "hello"); + Assert.True(result1.All(c => c.IsSuccess)); + + re.ClearWorkflows(); + workflow.Rules.First().LocalParams.First().Expression = "false"; + + re.AddWorkflow(workflow); + var result2 = await re.ExecuteAllRulesAsync("Test", "hello"); + Assert.True(result2.All(c => c.IsSuccess == false)); + } + + [Fact] + public async Task ExecuteRule_WithNullInput_ShouldNotThrowException() + { + var workflow = new Workflow { + WorkflowName = "Test", + Rules = new Rule[]{ + new Rule { + RuleName = "RuleWithLocalParam", + + RuleExpressionType = RuleExpressionType.LambdaExpression, + Expression = "input1 == null || input1.hello.world = \"wow\"" + } + } + }; + + var re = new RulesEngine(); + re.AddWorkflow(workflow); + + var result1 = await re.ExecuteAllRulesAsync("Test", new RuleParameter("input1", value: null)); + Assert.True(result1.All(c => c.IsSuccess)); + + + var result2 = await re.ExecuteAllRulesAsync("Test", new object[] { null }); + Assert.True(result2.All(c => c.IsSuccess)); + + dynamic input1 = new ExpandoObject(); + input1.hello = new ExpandoObject(); + input1.hello.world = "wow"; + + List result3 = await re.ExecuteAllRulesAsync("Test", input1); + Assert.True(result3.All(c => c.IsSuccess)); + + } + + [Fact] + public async Task ExecuteRule_SpecialCharInWorkflowName_RunsSuccessfully() + { + var workflow = new Workflow { + WorkflowName = "Exámple", + Rules = new Rule[]{ + new Rule { + RuleName = "RuleWithLocalParam", + + RuleExpressionType = RuleExpressionType.LambdaExpression, + Expression = "input1 == null || input1.hello.world = \"wow\"" + } + } + }; + + var workflowStr = "{\"WorkflowName\":\"Exámple\",\"WorkflowsToInject\":null,\"GlobalParams\":null,\"Rules\":[{\"RuleName\":\"RuleWithLocalParam\",\"Properties\":null,\"Operator\":null,\"ErrorMessage\":null,\"Enabled\":true,\"ErrorType\":\"Warning\",\"RuleExpressionType\":\"LambdaExpression\",\"WorkflowsToInject\":null,\"Rules\":null,\"LocalParams\":null,\"Expression\":\"input1 == null || input1.hello.world = \\\"wow\\\"\",\"Actions\":null,\"SuccessEvent\":null}]}"; + + var re = new RulesEngine(new string[] { workflowStr }, null); + + dynamic input1 = new ExpandoObject(); + input1.hello = new ExpandoObject(); + input1.hello.world = "wow"; + + List result3 = await re.ExecuteAllRulesAsync("Exámple", input1); + Assert.True(result3.All(c => c.IsSuccess)); + + } + + [Fact] + public void ContainsWorkFlowName_ShouldReturn() + { + const string ExistedWorkflowName = "ExistedWorkflowName"; + const string NotExistedWorkflowName = "NotExistedWorkflowName"; + + var workflow = new Workflow { + WorkflowName = ExistedWorkflowName, + Rules = new Rule[]{ + new Rule { + RuleName = "Rule", + RuleExpressionType = RuleExpressionType.LambdaExpression, + Expression = "1==1" + } + } + }; + + var re = new RulesEngine(); + re.AddWorkflow(workflow); + + Assert.True(re.ContainsWorkflow(ExistedWorkflowName)); + Assert.False(re.ContainsWorkflow(NotExistedWorkflowName)); + } + + [Theory] + [InlineData(typeof(RulesEngine), typeof(IRulesEngine))] + public void Class_PublicMethods_ArePartOfInterface(Type classType, Type interfaceType) + { + var classMethods = classType.GetMethods(BindingFlags.DeclaredOnly | + BindingFlags.Public | + BindingFlags.Instance); + + + var interfaceMethods = interfaceType.GetMethods(); + + + Assert.Equal(interfaceMethods.Count(), classMethods.Count()); + } + + + + private RulesEngine CreateRulesEngine(Workflow workflow) + { + var json = JsonConvert.SerializeObject(workflow); + return new RulesEngine(new string[] { json }, null); + } + + private RulesEngine GetRulesEngine(string filename, ReSettings reSettings = null) + { + var data = GetFileContent(filename); + + var injectWorkflow = new Workflow { + WorkflowName = "inputWorkflowReference", + WorkflowsToInject = new List { "inputWorkflow" } + }; + + var injectWorkflowStr = JsonConvert.SerializeObject(injectWorkflow); + return new RulesEngine(new string[] { data, injectWorkflowStr }, reSettings); + } + + private string GetFileContent(string filename) + { + var filePath = Path.Combine(Directory.GetCurrentDirectory() as string, "TestData", filename); + return File.ReadAllText(filePath); + } + + private Workflow ParseAsWorkflow(string WorkflowsFileName) + { + string content = GetFileContent(WorkflowsFileName); + return JsonConvert.DeserializeObject(content); + } + + private dynamic GetInput1() + { + var converter = new ExpandoObjectConverter(); + var basicInfo = "{\"name\": \"Dishant\",\"email\": \"abc@xyz.com\",\"creditHistory\": \"good\",\"country\": \"canada\",\"loyaltyFactor\": 3,\"totalPurchasesToDate\": 10000}"; + return JsonConvert.DeserializeObject(basicInfo, converter); + } + + private dynamic GetInput2() + { + var converter = new ExpandoObjectConverter(); + var orderInfo = "{\"totalOrders\": 5,\"recurringItems\": 2}"; + return JsonConvert.DeserializeObject(orderInfo, converter); + } + + private dynamic GetInput3() + { + var converter = new ExpandoObjectConverter(); + 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\": \"abc@xyz.com\",\"creditHistory\": \"good\",\"country\": \"canada\",\"loyaltyFactor\": 3,\"totalPurchasesToDate\": 70000}"; + var orderInfo = "{\"totalOrders\": 50,\"recurringItems\": 2}"; + var telemetryInfo = "{\"noOfVisitsPerMonth\": 10,\"percentageOfBuyingToVisit\": 15}"; + var laborCategoriesInput = "[{\"country\": \"india\", \"loyaltyFactor\": 2, \"totalPurchasesToDate\": 20000}]"; + var currentLaborCategoryInput = "{\"CurrentLaborCategoryProp\":\"TestVal2\"}"; + + dynamic input1 = JsonConvert.DeserializeObject>(laborCategoriesInput); + dynamic input2 = JsonConvert.DeserializeObject(currentLaborCategoryInput); + dynamic input3 = JsonConvert.DeserializeObject(telemetryInfo); + dynamic input4 = JsonConvert.DeserializeObject(basicInfo); + dynamic input5 = JsonConvert.DeserializeObject(orderInfo); + + var inputs = new dynamic[] + { + input1, + input2, + input3, + input4, + input5 + }; + + return inputs; + } + + [ExcludeFromCodeCoverage] + private class TestInstanceUtils + { + public bool CheckExists(string str) + { + if (!string.IsNullOrEmpty(str)) + { + return true; + } + + return false; + } + + } + + } } \ No newline at end of file diff --git a/test/RulesEngine.UnitTest/RuleCompilerTest.cs b/test/RulesEngine.UnitTest/RuleCompilerTest.cs index 5164b4a..72abd67 100644 --- a/test/RulesEngine.UnitTest/RuleCompilerTest.cs +++ b/test/RulesEngine.UnitTest/RuleCompilerTest.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.Extensions.Logging.Abstractions; using RulesEngine.ExpressionBuilders; using RulesEngine.Models; using System; @@ -17,10 +16,10 @@ namespace RulesEngine.UnitTest [Fact] public void RuleCompiler_NullCheck() { - Assert.Throws(() => new RuleCompiler(null, null,null)); + Assert.Throws(() => new RuleCompiler(null, null)); var reSettings = new ReSettings(); var parser = new RuleExpressionParser(reSettings); - Assert.Throws(() => new RuleCompiler(new RuleExpressionBuilderFactory(reSettings, parser), null,null)); + Assert.Throws(() => new RuleCompiler(null, null)); } [Fact] @@ -28,11 +27,9 @@ namespace RulesEngine.UnitTest { var reSettings = new ReSettings(); var parser = new RuleExpressionParser(reSettings); - var compiler = new RuleCompiler(new RuleExpressionBuilderFactory(reSettings, parser),null, new NullLogger()); + var compiler = new RuleCompiler(new RuleExpressionBuilderFactory(reSettings, parser),null); Assert.Throws(() => compiler.CompileRule(null, null,null)); Assert.Throws(() => compiler.CompileRule(null, new RuleParameter[] { null },null)); } - - } } diff --git a/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj b/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj index 6e99960..7cae76f 100644 --- a/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj +++ b/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj @@ -1,21 +1,21 @@  - netcoreapp3.1 + net6.0 True ..\..\signing\RulesEngine-publicKey.snk True - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -48,6 +48,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/test/RulesEngine.UnitTest/ScopedParamsTest.cs b/test/RulesEngine.UnitTest/ScopedParamsTest.cs index 70bf99b..f3900e7 100644 --- a/test/RulesEngine.UnitTest/ScopedParamsTest.cs +++ b/test/RulesEngine.UnitTest/ScopedParamsTest.cs @@ -26,7 +26,7 @@ namespace RulesEngine.UnitTest { var workflow = GetWorkflowList(); - var engine = new RulesEngine(null, null); + var engine = new RulesEngine(); engine.AddWorkflow(workflow); var input1 = new { @@ -47,7 +47,7 @@ namespace RulesEngine.UnitTest { var workflow = GetWorkflowList(); - var engine = new RulesEngine(null, null); + var engine = new RulesEngine(); engine.AddWorkflow(workflow); var input1 = new { @@ -77,7 +77,7 @@ namespace RulesEngine.UnitTest { var workflow = GetWorkflowList(); - var engine = new RulesEngine(new string[] { }, null, new ReSettings { + var engine = new RulesEngine(new string[] { }, new ReSettings { EnableScopedParams = false }); engine.AddWorkflow(workflow); diff --git a/test/RulesEngine.UnitTest/TestData/rules10.json b/test/RulesEngine.UnitTest/TestData/rules10.json new file mode 100644 index 0000000..59a3e9c --- /dev/null +++ b/test/RulesEngine.UnitTest/TestData/rules10.json @@ -0,0 +1,11 @@ +{ + "WorkflowName": "inputWorkflow", + "Rules": [ + { + "RuleName": "GiveDiscount10", + "SuccessEvent": "10", + "RuleExpressionType": "LambdaExpression", + "Expression": "input1.Data.GetProperty(\"category\").GetString() == \"abc\"" + } + ] + } \ No newline at end of file diff --git a/test/RulesEngine.UnitTest/TestData/rules5.json b/test/RulesEngine.UnitTest/TestData/rules5.json index c58afd1..0a0d731 100644 --- a/test/RulesEngine.UnitTest/TestData/rules5.json +++ b/test/RulesEngine.UnitTest/TestData/rules5.json @@ -2,12 +2,20 @@ "WorkflowName": "inputWorkflow", "Rules": [ { - "RuleName": "GiveDiscount10", + "RuleName": "upperCaseAccess", "SuccessEvent": "10", "ErrorMessage": "One or more adjust rules failed.", "ErrorType": "Error", "RuleExpressionType": "LambdaExpression", "Expression": "utils.CheckExists(String(input1.Property1)) == true" + }, + { + "RuleName": "lowerCaseAccess", + "SuccessEvent": "10", + "ErrorMessage": "One or more adjust rules failed.", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression", + "Expression": "utils.CheckExists(String(input1.property1)) == true" } ] } \ No newline at end of file