diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..9ec9a19 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-reportgenerator-globaltool": { + "version": "4.7.1", + "commands": [ + "reportgenerator" + ] + } + } +} \ No newline at end of file diff --git a/.github/workflows/dotnetcore-build.yml b/.github/workflows/dotnetcore-build.yml index 52de750..b88e702 100644 --- a/.github/workflows/dotnetcore-build.yml +++ b/.github/workflows/dotnetcore-build.yml @@ -4,7 +4,7 @@ on: push: branches: [ master ] pull_request: - branches: [ master ] + branches: [ master, develop ] jobs: build: @@ -16,21 +16,21 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.101 + dotnet-version: 3.1 - name: Install minicover run: dotnet tool install --global minicover --version 3.0.6 - name: Install dependencies - run: dotnet restore src/RulesEngine/RulesEngine.sln + run: dotnet restore RulesEngine.sln - name: Build - run: dotnet build src/RulesEngine/RulesEngine.sln --configuration Release --no-restore + run: dotnet build RulesEngine.sln --configuration Release --no-restore - name: Instrument run: minicover instrument - name: Test - run: dotnet test src/RulesEngine/RulesEngine.sln --no-build --configuration Release --verbosity m + run: dotnet test RulesEngine.sln --no-build --configuration Release --verbosity m - name: Uninstrument run: minicover uninstrument - name: Report - run: minicover report --threshold 80 + run: minicover report --threshold 95 if: ${{ github.event_name == 'pull_request' }} - name: Report coveralls run: minicover coverallsreport --repo-token ${{ secrets.COVERALLS_TOKEN }} --branch master diff --git a/.gitignore b/.gitignore index 85836cc..728408d 100644 --- a/.gitignore +++ b/.gitignore @@ -330,3 +330,4 @@ ASALocalRun/ .mfractor/ /src/RulesEngine/RulesEngine.sln.licenseheader /assets/RulesEnginePackageFile.xml +coveragereport/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..6f691ce --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/demo/DemoApp/bin/Debug/netcoreapp3.1/DemoApp.dll", + "args": [], + "cwd": "${workspaceFolder}/demo/DemoApp", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..13c9d39 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "dotnetCoreExplorer.searchpatterns": "test/**/bin/Debug/netcoreapp*/*.{dll,exe,json}", + "coverage-gutters.coverageBaseDir": "coveragereport", + "coverage-gutters.coverageReportFileName": "coveragereport/**/index.html" +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..94770e3 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/demo/DemoApp/DemoApp.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/demo/DemoApp/DemoApp.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "${workspaceFolder}/demo/DemoApp/DemoApp.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6940cbb..3fe4e77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,9 @@ # CHANGELOG All notable changes to this project will be documented in this file. -## [2.1.4] - 15-10-2020 -- Added exception data properties to identify RuleName. - -## [2.1.3] - 12-10-2020 -- Optional parameter for rethrow exception on failure of expression compilation. - -## [2.1.2] - 02-10-2020 -- Fixed binary expression requirement. Now any expression will work as long as it evalutes to boolean. - -## [2.1.1] - 01-09-2020 -- Fixed exception thrown when errormessage field is null -- Added better messaging when identifier is not found in expression -- Fixed other minor bugs +## [3.0.0-preview.1] - 23-10-2020 +- Renamed `ExecuteRule` to `ExecuteAllRulesAsync` +- Added Actions support. More details on [actions wiki](https://github.com/microsoft/RulesEngine/wiki/Actions) ## [2.1.0] - 18-05-2020 - Adding local param support to make expression authroing more intuitive. diff --git a/README.md b/README.md index 4287095..0c446ea 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,9 @@ var rulesEngine = new RulesEngine(workflowRules, logger); ``` Here, *workflowRules* is a list of deserialized object based out of the schema explained above and *logger* is a custom logger instance made out of an [ILogger](https://github.com/microsoft/RulesEngine/wiki/Getting-Started#logger) instance. -Once done, the Rules Engine needs to execute the rules for a given input. It can be done by calling the method ExecuteRule as shown below - +Once done, the Rules Engine needs to execute the rules for a given input. It can be done by calling the method ExecuteAllRulesAsync as shown below - ```c# -List response = rulesEngine.ExecuteRule(workflowName, input); +List response = await rulesEngine.ExecuteAllRulesAsync(workflowName, input); ``` Here, *workflowName* is the name of the workflow, which is *Discount* in the above mentioned example. And *input* is the object which needs to be checked against the rules. diff --git a/src/RulesEngine/RulesEngine.sln b/RulesEngine.sln similarity index 89% rename from src/RulesEngine/RulesEngine.sln rename to RulesEngine.sln index bb5e076..f984879 100644 --- a/src/RulesEngine/RulesEngine.sln +++ b/RulesEngine.sln @@ -3,11 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29123.89 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RulesEngine", "RulesEngine\RulesEngine.csproj", "{CD4DFE6A-083B-478E-8377-77F474833E30}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RulesEngine", "src\RulesEngine\\RulesEngine.csproj", "{CD4DFE6A-083B-478E-8377-77F474833E30}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RulesEngine.UnitTest", "..\..\test\RulesEngine.UnitTest\RulesEngine.UnitTest.csproj", "{50E0C2A5-E2C8-4B12-8C0E-B69F698A82BF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RulesEngine.UnitTest", "test\RulesEngine.UnitTest\RulesEngine.UnitTest.csproj", "{50E0C2A5-E2C8-4B12-8C0E-B69F698A82BF}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DemoApp", "..\..\demo\DemoApp\DemoApp.csproj", "{57BB8C07-799A-4F87-A7CC-D3D3F694DD02}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DemoApp", "demo\DemoApp\DemoApp.csproj", "{57BB8C07-799A-4F87-A7CC-D3D3F694DD02}" ProjectSection(ProjectDependencies) = postProject {CD4DFE6A-083B-478E-8377-77F474833E30} = {CD4DFE6A-083B-478E-8377-77F474833E30} EndProjectSection diff --git a/demo/DemoApp/BasicDemo.cs b/demo/DemoApp/BasicDemo.cs index 9812b7e..7036650 100644 --- a/demo/DemoApp/BasicDemo.cs +++ b/demo/DemoApp/BasicDemo.cs @@ -42,7 +42,7 @@ namespace DemoApp string discountOffered = "No discount offered."; - List resultList = bre.ExecuteRule("Discount", inputs); + List resultList = bre.ExecuteAllRulesAsync("Discount", inputs).Result; resultList.OnSuccess((eventName) => { diff --git a/demo/DemoApp/DemoApp.csproj b/demo/DemoApp/DemoApp.csproj index 37c2e70..fc7049e 100644 --- a/demo/DemoApp/DemoApp.csproj +++ b/demo/DemoApp/DemoApp.csproj @@ -8,7 +8,7 @@ - + diff --git a/demo/DemoApp/NestedInputDemo.cs b/demo/DemoApp/NestedInputDemo.cs index 95c6bca..9dd9afe 100644 --- a/demo/DemoApp/NestedInputDemo.cs +++ b/demo/DemoApp/NestedInputDemo.cs @@ -50,7 +50,7 @@ namespace DemoApp var bre = new RulesEngine.RulesEngine(workflowRules.ToArray(),null); foreach(var workflow in workflowRules) { - List resultList = bre.ExecuteRule(workflow.WorkflowName, nestedInput); + List resultList = bre.ExecuteAllRulesAsync(workflow.WorkflowName, nestedInput).Result; resultList.OnSuccess((eventName) => { diff --git a/global.json b/global.json index 0d92626..d36fe28 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,7 @@ { "sdk": { - "version": "3.1.101", - "rollForward": "latestFeature" + "version": "3.1", + "rollForward": "latestFeature", + "allowPrerelease": false } } \ No newline at end of file diff --git a/src/RulesEngine/Actions/ActionBase.cs b/src/RulesEngine/Actions/ActionBase.cs new file mode 100644 index 0000000..bfbbd66 --- /dev/null +++ b/src/RulesEngine/Actions/ActionBase.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using RulesEngine.Models; + +namespace RulesEngine.Actions +{ + public abstract class ActionBase + { + internal virtual async ValueTask ExecuteAndReturnResultAsync(ActionContext context, RuleParameter[] ruleParameters,bool includeRuleResults=false){ + ActionRuleResult result = new ActionRuleResult(); + try + { + result.Output = await Run(context, ruleParameters); + } + catch(Exception ex) + { + result.Exception = new Exception($"Exception while executing {this.GetType().Name}: {ex.Message}",ex); + } + finally + { + if(includeRuleResults){ + result.Results = new List() + { + context.GetParentRuleResult() + }; + } + } + return result; + } + public abstract ValueTask Run(ActionContext context, RuleParameter[] ruleParameters); + } +} diff --git a/src/RulesEngine/Actions/ActionContext.cs b/src/RulesEngine/Actions/ActionContext.cs new file mode 100644 index 0000000..6361ce8 --- /dev/null +++ b/src/RulesEngine/Actions/ActionContext.cs @@ -0,0 +1,48 @@ +using Newtonsoft.Json; +using RulesEngine.Models; +using System; +using System.Collections.Generic; + +namespace RulesEngine.Actions +{ + public class ActionContext + { + private readonly IDictionary _context; + private readonly RuleResultTree _parentResult; + + public ActionContext(IDictionary context, RuleResultTree parentResult) + { + _context = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var kv in context) + { + string key = kv.Key; + string value = kv.Value is string ? kv.Value.ToString() : JsonConvert.SerializeObject(kv.Value); + _context.Add(key, value); + } + _parentResult = parentResult; + } + + public RuleResultTree GetParentRuleResult(){ + return _parentResult; + } + public T GetContext(string name) + { + try + { + if (typeof(T) == typeof(string)) + { + return (T)Convert.ChangeType(_context[name], typeof(T)); + } + return JsonConvert.DeserializeObject(_context[name]); + } + catch (KeyNotFoundException) + { + throw new ArgumentException($"Argument `{name}` was not found in the action context"); + } + catch (JsonException) + { + throw new ArgumentException($"Failed to convert argument `{name}` to type `{typeof(T).Name}` in the action context"); + } + } + } +} diff --git a/src/RulesEngine/Actions/ActionFactory.cs b/src/RulesEngine/Actions/ActionFactory.cs new file mode 100644 index 0000000..2b7b504 --- /dev/null +++ b/src/RulesEngine/Actions/ActionFactory.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; + +namespace RulesEngine.Actions +{ + internal class ActionFactory + { + private readonly IDictionary> _actionRegistry; + + internal ActionFactory() + { + _actionRegistry = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + } + internal ActionFactory(IDictionary> actionRegistry): this() + { + foreach(var kv in actionRegistry){ + _actionRegistry.Add(kv.Key, kv.Value); + } + } + + internal ActionBase Get(string name) + { + if (_actionRegistry.ContainsKey(name)) + { + return _actionRegistry[name](); + } + throw new KeyNotFoundException($"Action with name:{name} does not exist"); + } + } +} diff --git a/src/RulesEngine/Actions/EvaluateRuleAction.cs b/src/RulesEngine/Actions/EvaluateRuleAction.cs new file mode 100644 index 0000000..605d2d2 --- /dev/null +++ b/src/RulesEngine/Actions/EvaluateRuleAction.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using RulesEngine.ExpressionBuilders; +using RulesEngine.Models; + +namespace RulesEngine.Actions +{ + public class EvaluateRuleAction : ActionBase + { + private readonly RulesEngine _ruleEngine; + + public EvaluateRuleAction(RulesEngine ruleEngine) + { + _ruleEngine = ruleEngine; + } + + internal override async ValueTask ExecuteAndReturnResultAsync(ActionContext context, RuleParameter[] ruleParameters, bool includeRuleResults=false){ + var innerResult = await base.ExecuteAndReturnResultAsync(context,ruleParameters,includeRuleResults); + var output = innerResult.Output as ActionRuleResult; + List resultList = null; + if(includeRuleResults){ + resultList = new List(output.Results); + resultList.AddRange(innerResult.Results); + } + return new ActionRuleResult { + Output = output.Output, + Exception = innerResult.Exception, + Results = resultList + }; + } + + public override async ValueTask Run(ActionContext context, RuleParameter[] ruleParameters) + { + var workflowName = context.GetContext("workflowName"); + var ruleName = context.GetContext("ruleName"); + var ruleResult = await _ruleEngine.ExecuteActionWorkflowAsync(workflowName,ruleName,ruleParameters); + return ruleResult; + } + } +} diff --git a/src/RulesEngine/Actions/ExpressionOutputAction.cs b/src/RulesEngine/Actions/ExpressionOutputAction.cs new file mode 100644 index 0000000..eec7215 --- /dev/null +++ b/src/RulesEngine/Actions/ExpressionOutputAction.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using RulesEngine.ExpressionBuilders; +using RulesEngine.Models; + +namespace RulesEngine.Actions +{ + public class OutputExpressionAction : ActionBase + { + private readonly RuleExpressionParser _ruleExpressionParser; + + public OutputExpressionAction(RuleExpressionParser ruleExpressionParser) + { + _ruleExpressionParser = ruleExpressionParser; + } + + public override ValueTask Run(ActionContext context, RuleParameter[] ruleParameters) + { + var expression = context.GetContext("expression"); + return new ValueTask(_ruleExpressionParser.Evaluate(expression, ruleParameters)); + } + } +} diff --git a/src/RulesEngine/RulesEngine/CustomTypeProvider.cs b/src/RulesEngine/CustomTypeProvider.cs similarity index 100% rename from src/RulesEngine/RulesEngine/CustomTypeProvider.cs rename to src/RulesEngine/CustomTypeProvider.cs diff --git a/src/RulesEngine/Enums/ActionTriggerType.cs b/src/RulesEngine/Enums/ActionTriggerType.cs new file mode 100644 index 0000000..e2854e7 --- /dev/null +++ b/src/RulesEngine/Enums/ActionTriggerType.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace RulesEngine.Enums +{ + public enum ActionTriggerType + { + onSuccess, + onFailure + } +} diff --git a/src/RulesEngine/RulesEngine/Exceptions/RuleValidationException.cs b/src/RulesEngine/Exceptions/RuleValidationException.cs similarity index 100% rename from src/RulesEngine/RulesEngine/Exceptions/RuleValidationException.cs rename to src/RulesEngine/Exceptions/RuleValidationException.cs diff --git a/src/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs b/src/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs new file mode 100644 index 0000000..acf978a --- /dev/null +++ b/src/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine.HelperFunctions; +using RulesEngine.Models; +using System; + +namespace RulesEngine.ExpressionBuilders +{ + /// + /// This class will build the list expression + /// + internal sealed class LambdaExpressionBuilder : RuleExpressionBuilderBase + { + private readonly ReSettings _reSettings; + private readonly RuleExpressionParser _ruleExpressionParser; + + internal LambdaExpressionBuilder(ReSettings reSettings, RuleExpressionParser ruleExpressionParser) + { + _reSettings = reSettings; + _ruleExpressionParser = ruleExpressionParser; + } + + internal override RuleFunc BuildDelegateForRule(Rule rule, RuleParameter[] ruleParams) + { + try + { + var ruleDelegate = _ruleExpressionParser.Compile(rule.Expression, ruleParams,typeof(bool)); + bool func(object[] paramList) => (bool)ruleDelegate.DynamicInvoke(paramList); + return Helpers.ToResultTree(rule, null, func); + } + catch (Exception ex) + { + ex.Data.Add(nameof(rule.RuleName), rule.RuleName); + ex.Data.Add(nameof(rule.Expression), rule.Expression); + + if (!_reSettings.EnableExceptionAsErrorMessage) throw; + bool func(object[] param) => false; + var exceptionMessage = $"Exception while parsing expression `{rule?.Expression}` - {ex.Message}"; + return Helpers.ToResultTree(rule, null, func, exceptionMessage); + } + } + } +} diff --git a/src/RulesEngine/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs b/src/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs similarity index 51% rename from src/RulesEngine/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs rename to src/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs index 88a6da7..84966ec 100644 --- a/src/RulesEngine/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs +++ b/src/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs @@ -20,13 +20,6 @@ namespace RulesEngine.ExpressionBuilders /// The type parameter expressions. /// The rule input exp. /// Expression type - internal abstract Expression> BuildExpressionForRule(Rule rule, IEnumerable typeParamExpressions, ParameterExpression ruleInputExp); - - /// Builds the expression for rule parameter. - /// The rule. - /// The type parameter expressions. - /// The rule input exp. - /// Expression. - internal abstract Expression BuildExpressionForRuleParam(LocalParam rule, IEnumerable typeParamExpressions, ParameterExpression ruleInputExp); + internal abstract RuleFunc BuildDelegateForRule(Rule rule, RuleParameter[] ruleParams); } } diff --git a/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs b/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs new file mode 100644 index 0000000..ae0c279 --- /dev/null +++ b/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs @@ -0,0 +1,73 @@ +using Microsoft.Extensions.Caching.Memory; +using RulesEngine.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Linq.Expressions; + +namespace RulesEngine.ExpressionBuilders +{ + public class RuleExpressionParser + { + private readonly ReSettings _reSettings; + private static IMemoryCache _memoryCache; + + public RuleExpressionParser(ReSettings reSettings) + { + _reSettings = reSettings; + _memoryCache = new MemoryCache(new MemoryCacheOptions{ + SizeLimit = 1000 + }); + } + + public Delegate Compile(string expression, RuleParameter[] ruleParams, Type returnType = null) + { + var cacheKey = GetCacheKey(expression,ruleParams,returnType); + return _memoryCache.GetOrCreate(cacheKey,(entry) => { + entry.SetSize(1); + var config = new ParsingConfig { CustomTypeProvider = new CustomTypeProvider(_reSettings.CustomTypes) }; + var typeParamExpressions = GetParameterExpression(ruleParams).ToArray(); + var e = DynamicExpressionParser.ParseLambda(config, true, typeParamExpressions.ToArray(), returnType, expression); + return e.Compile(); + }); + + } + + public object Evaluate(string expression, RuleParameter[] ruleParams, Type returnType = null) + { + var func = Compile(expression, ruleParams, returnType); + return func.DynamicInvoke(ruleParams.Select(c => c.Value).ToArray()); + } + + // + /// Gets the parameter expression. + /// + /// The types. + /// + /// + /// types + /// or + /// type + /// + private IEnumerable GetParameterExpression(params RuleParameter[] ruleParams) + { + foreach (var ruleParam in ruleParams) + { + if (ruleParam == null) + { + throw new ArgumentException($"{nameof(ruleParam)} can't be null."); + } + + yield return Expression.Parameter(ruleParam.Type, ruleParam.Name); + } + } + + private string GetCacheKey(string expression, RuleParameter[] ruleParameters,Type returnType){ + var paramKey = string.Join("|",ruleParameters.Select(c => c.Type.ToString())); + var returnTypeKey = returnType?.ToString() ?? "null"; + var combined = $"Expression:{expression}-Params:{paramKey}-ReturnType:{returnTypeKey}"; + return combined; + } + } +} diff --git a/src/RulesEngine/RulesEngine/Extensions/ListofRuleResultTreeExtension.cs b/src/RulesEngine/Extensions/ListofRuleResultTreeExtension.cs similarity index 100% rename from src/RulesEngine/RulesEngine/Extensions/ListofRuleResultTreeExtension.cs rename to src/RulesEngine/Extensions/ListofRuleResultTreeExtension.cs diff --git a/src/RulesEngine/RulesEngine/HelperFunctions/Constants.cs b/src/RulesEngine/HelperFunctions/Constants.cs similarity index 100% rename from src/RulesEngine/RulesEngine/HelperFunctions/Constants.cs rename to src/RulesEngine/HelperFunctions/Constants.cs diff --git a/src/RulesEngine/RulesEngine/HelperFunctions/ExpressionUtils.cs b/src/RulesEngine/HelperFunctions/ExpressionUtils.cs similarity index 100% rename from src/RulesEngine/RulesEngine/HelperFunctions/ExpressionUtils.cs rename to src/RulesEngine/HelperFunctions/ExpressionUtils.cs diff --git a/src/RulesEngine/HelperFunctions/Helpers.cs b/src/RulesEngine/HelperFunctions/Helpers.cs new file mode 100644 index 0000000..dbd382c --- /dev/null +++ b/src/RulesEngine/HelperFunctions/Helpers.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace RulesEngine.HelperFunctions +{ + /// + /// Helpers + /// + internal static class Helpers + { + internal static RuleFunc ToResultTree(Rule rule, IEnumerable childRuleResults, Func isSuccessFunc, string exceptionMessage = "") + { + return (inputs) => new RuleResultTree + { + Rule = rule, + Inputs = inputs.ToDictionary(c => c.Name,c => c.Value), + IsSuccess = isSuccessFunc(inputs.Select(c => c.Value).ToArray()), + ChildResults = childRuleResults, + ExceptionMessage = exceptionMessage + }; + } + + /// + /// To the result tree error messages + /// + /// ruleResultTree + /// ruleResultMessage + internal static void ToResultTreeMessages(RuleResultTree ruleResultTree, ref RuleResultMessage ruleResultMessage) + { + if (ruleResultTree.ChildResults != null) + { + GetChildRuleMessages(ruleResultTree.ChildResults, ref ruleResultMessage); + } + else + { + if (!ruleResultTree.IsSuccess) + { + string errMsg = ruleResultTree.Rule.ErrorMessage; + errMsg = string.IsNullOrEmpty(errMsg) ? $"Error message does not configured for {ruleResultTree.Rule.RuleName}" : errMsg; + + if (ruleResultTree.Rule.ErrorType == ErrorType.Error && !ruleResultMessage.ErrorMessages.Contains(errMsg)) + { + ruleResultMessage.ErrorMessages.Add(errMsg); + } + else if (ruleResultTree.Rule.ErrorType == ErrorType.Warning && !ruleResultMessage.WarningMessages.Contains(errMsg)) + { + ruleResultMessage.WarningMessages.Add(errMsg); + } + } + } + } + + /// + /// To get the child error message recersivly + /// + /// childResultTree + /// ruleResultMessage + private static void GetChildRuleMessages(IEnumerable childResultTree, ref RuleResultMessage ruleResultMessage) + { + foreach (var item in childResultTree) + { + ToResultTreeMessages(item, ref ruleResultMessage); + } + } + } +} diff --git a/src/RulesEngine/RulesEngine/HelperFunctions/Utils.cs b/src/RulesEngine/HelperFunctions/Utils.cs similarity index 99% rename from src/RulesEngine/RulesEngine/HelperFunctions/Utils.cs rename to src/RulesEngine/HelperFunctions/Utils.cs index d02c700..f22f8a8 100644 --- a/src/RulesEngine/RulesEngine/HelperFunctions/Utils.cs +++ b/src/RulesEngine/HelperFunctions/Utils.cs @@ -107,6 +107,7 @@ namespace RulesEngine.HelperFunctions return obj; } + private static IEnumerable Cast(this IEnumerable self, Type innerType) { var methodInfo = typeof(Enumerable).GetMethod("Cast"); @@ -121,4 +122,6 @@ namespace RulesEngine.HelperFunctions return genericMethod.Invoke(null, new[] { self }) as IList; } } + + } diff --git a/src/RulesEngine/RulesEngine/Interfaces/IRulesEngine.cs b/src/RulesEngine/Interfaces/IRulesEngine.cs similarity index 63% rename from src/RulesEngine/RulesEngine/Interfaces/IRulesEngine.cs rename to src/RulesEngine/Interfaces/IRulesEngine.cs index 3c1d717..7922930 100644 --- a/src/RulesEngine/RulesEngine/Interfaces/IRulesEngine.cs +++ b/src/RulesEngine/Interfaces/IRulesEngine.cs @@ -3,6 +3,7 @@ using RulesEngine.Models; using System.Collections.Generic; +using System.Threading.Tasks; namespace RulesEngine.Interfaces { @@ -14,7 +15,7 @@ namespace RulesEngine.Interfaces /// The name of the workflow with rules to execute against the inputs /// A variable number of inputs /// List of rule results - List ExecuteRule(string workflowName, params object[] inputs); + ValueTask> ExecuteAllRulesAsync(string workflowName, params object[] inputs); /// /// This will execute all the rules of the specified workflow @@ -22,6 +23,10 @@ namespace RulesEngine.Interfaces /// The name of the workflow with rules to execute against the inputs /// A variable number of rule parameters /// List of rule results - List ExecuteRule(string workflowName, params RuleParameter[] ruleParams); + ValueTask> ExecuteAllRulesAsync(string workflowName, params RuleParameter[] ruleParams); + ValueTask ExecuteActionWorkflowAsync(string workflowName, string ruleName, RuleParameter[] ruleParameters); + void AddWorkflow(params WorkflowRules[] workflowRules); + void ClearWorkflows(); + void RemoveWorkflow(params string[] workflowNames); } } diff --git a/src/RulesEngine/Models/ActionInfo.cs b/src/RulesEngine/Models/ActionInfo.cs new file mode 100644 index 0000000..7964a91 --- /dev/null +++ b/src/RulesEngine/Models/ActionInfo.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace RulesEngine.Models +{ + public class ActionInfo + { + public string Name { get; set; } + public Dictionary Context { get; set; } + } +} diff --git a/src/RulesEngine/Models/ActionResult.cs b/src/RulesEngine/Models/ActionResult.cs new file mode 100644 index 0000000..30a4ebc --- /dev/null +++ b/src/RulesEngine/Models/ActionResult.cs @@ -0,0 +1,6 @@ +using System; + +public class ActionResult{ + public object Output {get; set;} + public Exception Exception { get; set; } +} \ No newline at end of file diff --git a/src/RulesEngine/Models/ActionRuleResult.cs b/src/RulesEngine/Models/ActionRuleResult.cs new file mode 100644 index 0000000..ba84d30 --- /dev/null +++ b/src/RulesEngine/Models/ActionRuleResult.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; +using RulesEngine.Models; + + +public class ActionRuleResult : ActionResult{ + public List Results {get; set;} +} \ No newline at end of file diff --git a/src/RulesEngine/RulesEngine/Models/CompiledParam.cs b/src/RulesEngine/Models/CompiledParam.cs similarity index 79% rename from src/RulesEngine/RulesEngine/Models/CompiledParam.cs rename to src/RulesEngine/Models/CompiledParam.cs index c05b968..676b9f8 100644 --- a/src/RulesEngine/RulesEngine/Models/CompiledParam.cs +++ b/src/RulesEngine/Models/CompiledParam.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text; namespace RulesEngine.Models @@ -7,6 +8,7 @@ namespace RulesEngine.Models /// /// CompiledParam class. /// + [ExcludeFromCodeCoverage] internal class CompiledParam { /// @@ -32,5 +34,10 @@ namespace RulesEngine.Models /// The parameters. /// internal IEnumerable Parameters { get; set; } + + internal RuleParameter AsRuleParameter() + { + return new RuleParameter(Name,Value.Method.ReturnType); + } } } diff --git a/src/RulesEngine/RulesEngine/Models/LocalParam.cs b/src/RulesEngine/Models/LocalParam.cs similarity index 81% rename from src/RulesEngine/RulesEngine/Models/LocalParam.cs rename to src/RulesEngine/Models/LocalParam.cs index 754e0be..302472b 100644 --- a/src/RulesEngine/RulesEngine/Models/LocalParam.cs +++ b/src/RulesEngine/Models/LocalParam.cs @@ -1,9 +1,11 @@ using Newtonsoft.Json; +using System.Diagnostics.CodeAnalysis; namespace RulesEngine.Models { - /// Class Param. - /// Implements the + /// Class LocalParam. + /// + [ExcludeFromCodeCoverage] public class LocalParam { diff --git a/src/RulesEngine/RulesEngine/Models/ReSettings.cs b/src/RulesEngine/Models/ReSettings.cs similarity index 71% rename from src/RulesEngine/RulesEngine/Models/ReSettings.cs rename to src/RulesEngine/Models/ReSettings.cs index b1426be..d922289 100644 --- a/src/RulesEngine/RulesEngine/Models/ReSettings.cs +++ b/src/RulesEngine/Models/ReSettings.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using RulesEngine.Actions; using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace RulesEngine.Models @@ -9,7 +11,10 @@ namespace RulesEngine.Models [ExcludeFromCodeCoverage] public class ReSettings { - public bool EnableExceptionAsErrorMessage { get; set; } = true; public Type[] CustomTypes { get; set; } + + public Dictionary> CustomActions { get; set; } + + public bool EnableExceptionAsErrorMessage { get; set; } = true; } } diff --git a/src/RulesEngine/Models/Rule.cs b/src/RulesEngine/Models/Rule.cs new file mode 100644 index 0000000..5be2937 --- /dev/null +++ b/src/RulesEngine/Models/Rule.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using RulesEngine.Enums; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace RulesEngine.Models +{ + /// + /// Rule class + /// + [ExcludeFromCodeCoverage] + public class Rule + { + public string RuleName { get; set; } + public string Operator { get; set; } + public string ErrorMessage { get; set; } + + [JsonConverter(typeof(StringEnumConverter))] + public ErrorType ErrorType { get; set; } + + [JsonConverter(typeof(StringEnumConverter))] + public RuleExpressionType? RuleExpressionType { get; set; } + + public List WorkflowRulesToInject { get; set; } + + public List Rules { get; set; } + + [JsonProperty] + public IEnumerable LocalParams { get; private set; } + public string Expression { get; set; } + + public Dictionary Actions { get; set; } + public string SuccessEvent { get; set; } + + } +} diff --git a/src/RulesEngine/Models/RuleDelegate.cs b/src/RulesEngine/Models/RuleDelegate.cs new file mode 100644 index 0000000..7102930 --- /dev/null +++ b/src/RulesEngine/Models/RuleDelegate.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace RulesEngine.Models +{ + public delegate T RuleFunc(params RuleParameter[] ruleParameters); +} diff --git a/src/RulesEngine/RulesEngine/Models/RuleErrorType.cs b/src/RulesEngine/Models/RuleErrorType.cs similarity index 100% rename from src/RulesEngine/RulesEngine/Models/RuleErrorType.cs rename to src/RulesEngine/Models/RuleErrorType.cs diff --git a/src/RulesEngine/RulesEngine/Models/RuleExpressionType.cs b/src/RulesEngine/Models/RuleExpressionType.cs similarity index 100% rename from src/RulesEngine/RulesEngine/Models/RuleExpressionType.cs rename to src/RulesEngine/Models/RuleExpressionType.cs diff --git a/src/RulesEngine/RulesEngine/Models/RuleParameter.cs b/src/RulesEngine/Models/RuleParameter.cs similarity index 83% rename from src/RulesEngine/RulesEngine/Models/RuleParameter.cs rename to src/RulesEngine/Models/RuleParameter.cs index 639a26f..a000494 100644 --- a/src/RulesEngine/RulesEngine/Models/RuleParameter.cs +++ b/src/RulesEngine/Models/RuleParameter.cs @@ -17,6 +17,10 @@ namespace RulesEngine.Models Name = name; } + internal RuleParameter(string name,Type type){ + Name = name; + Type = type; + } public Type Type { get; } public string Name { get; } public object Value { get; } diff --git a/src/RulesEngine/RulesEngine/Models/RuleResultTree.cs b/src/RulesEngine/Models/RuleResultTree.cs similarity index 96% rename from src/RulesEngine/RulesEngine/Models/RuleResultTree.cs rename to src/RulesEngine/Models/RuleResultTree.cs index fab28d1..a4e392b 100644 --- a/src/RulesEngine/RulesEngine/Models/RuleResultTree.cs +++ b/src/RulesEngine/Models/RuleResultTree.cs @@ -40,7 +40,9 @@ namespace RulesEngine.Models /// /// Gets or sets the input object /// - public object Input { get; set; } + public Dictionary Inputs { get; set; } + + public ActionResult ActionResult {get; set;} /// /// Gets the exception message in case an error is thrown during rules calculation. diff --git a/src/RulesEngine/RulesEngine/Models/WorkflowRules.cs b/src/RulesEngine/Models/WorkflowRules.cs similarity index 100% rename from src/RulesEngine/RulesEngine/Models/WorkflowRules.cs rename to src/RulesEngine/Models/WorkflowRules.cs diff --git a/src/RulesEngine/ParamCompiler.cs b/src/RulesEngine/ParamCompiler.cs new file mode 100644 index 0000000..5c09c6c --- /dev/null +++ b/src/RulesEngine/ParamCompiler.cs @@ -0,0 +1,78 @@ +using Microsoft.Extensions.Logging; +using RulesEngine.Models; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text; +using System.Linq; +using RulesEngine.ExpressionBuilders; + +namespace RulesEngine +{ + /// + /// Rule param compilers + /// + internal class ParamCompiler + { + + private readonly ReSettings _reSettings; + private readonly RuleExpressionParser _ruleExpressionParser; + + internal ParamCompiler(ReSettings reSettings, RuleExpressionParser ruleExpressionParser) + { + _reSettings = reSettings; + _ruleExpressionParser = ruleExpressionParser; + } + + /// + /// Compiles the and evaluate parameter expression. + /// + /// The rule. + /// The rule parameters. + /// + /// IEnumerable<RuleParameter>. + /// + public IEnumerable CompileParamsExpression(Rule rule, IEnumerable ruleParams) + { + + if(rule.LocalParams == null) return null; + + var compiledParameters = new List(); + var evaluatedParameters = new List(); + foreach (var param in rule.LocalParams) + { + var compiledParam = GetDelegateForRuleParam(param, ruleParams.ToArray()); + compiledParameters.Add(new CompiledParam { Name = param.Name, Value = compiledParam, Parameters = evaluatedParameters }); + var evaluatedParam = EvaluateCompiledParam(param.Name, compiledParam, ruleParams); + ruleParams = ruleParams.Append(evaluatedParam); + evaluatedParameters.Add(evaluatedParam); + } + + return compiledParameters; + } + + /// Evaluates the compiled parameter. + /// Name of the parameter. + /// The compiled parameter. + /// The rule parameters. + /// RuleParameter. + public RuleParameter EvaluateCompiledParam(string paramName, Delegate compiledParam, IEnumerable inputs) + { + var result = compiledParam.DynamicInvoke(inputs.Select(c => c.Value).ToArray()); + return new RuleParameter(paramName, result); + } + + + /// + /// Gets the expression for rule. + /// + /// The rule. + /// The type parameter expressions. + /// The rule input exp. + /// + private Delegate GetDelegateForRuleParam(LocalParam param, RuleParameter[] ruleParameters) + { + return _ruleExpressionParser.Compile(param.Expression, ruleParameters); + } + } +} diff --git a/src/RulesEngine/RulesEngine/Properties/AssemblyInfo.cs b/src/RulesEngine/Properties/AssemblyInfo.cs similarity index 100% rename from src/RulesEngine/RulesEngine/Properties/AssemblyInfo.cs rename to src/RulesEngine/Properties/AssemblyInfo.cs diff --git a/src/RulesEngine/RuleCompiler.cs b/src/RulesEngine/RuleCompiler.cs new file mode 100644 index 0000000..0084772 --- /dev/null +++ b/src/RulesEngine/RuleCompiler.cs @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.Logging; +using RulesEngine.HelperFunctions; +using RulesEngine.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace RulesEngine +{ + /// + /// Rule compilers + /// + internal class RuleCompiler + { + /// + /// The nested operators + /// + private readonly ExpressionType[] nestedOperators = new ExpressionType[] { ExpressionType.And, ExpressionType.AndAlso, ExpressionType.Or, ExpressionType.OrElse }; + + /// + /// The expression builder factory + /// + private readonly RuleExpressionBuilderFactory _expressionBuilderFactory; + + /// + /// The logger + /// + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The expression builder factory. + /// expressionBuilderFactory + internal RuleCompiler(RuleExpressionBuilderFactory expressionBuilderFactory, ILogger logger) + { + if (expressionBuilderFactory == null) + { + throw new ArgumentNullException($"{nameof(expressionBuilderFactory)} can't be null."); + } + + if (logger == null) + { + throw new ArgumentNullException($"{nameof(logger)} can't be null."); + } + + _logger = logger; + _expressionBuilderFactory = expressionBuilderFactory; + } + + /// + /// Compiles the rule + /// + /// + /// + /// + /// + /// Compiled func delegate + internal RuleFunc CompileRule(Rule rule,params RuleParameter[] ruleParams) + { + try + { + if(rule == null) + { + throw new ArgumentNullException(nameof(rule)); + } + RuleFunc ruleExpression = GetDelegateForRule(rule,ruleParams); + return ruleExpression; + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + throw; + } + } + + + + /// + /// Gets the expression for rule. + /// + /// The rule. + /// The type parameter expressions. + /// The rule input exp. + /// + private RuleFunc GetDelegateForRule(Rule rule, RuleParameter[] ruleParams) + { + ExpressionType nestedOperator; + + if (Enum.TryParse(rule.Operator, out nestedOperator) && nestedOperators.Contains(nestedOperator) && + rule.Rules != null && rule.Rules.Any()) + { + return BuildNestedRuleFunc(rule, nestedOperator, ruleParams); + } + else + { + return BuildRuleFunc(rule, ruleParams); + } + } + + /// + /// Builds the expression. + /// + /// The rule. + /// The type parameter expressions. + /// The rule input exp. + /// + /// + private RuleFunc BuildRuleFunc(Rule rule, RuleParameter[] ruleParams) + { + if (!rule.RuleExpressionType.HasValue) + { + throw new InvalidOperationException($"RuleExpressionType can not be null for leaf level expressions."); + } + + var ruleExpressionBuilder = _expressionBuilderFactory.RuleGetExpressionBuilder(rule.RuleExpressionType.Value); + + var ruleFunc = ruleExpressionBuilder.BuildDelegateForRule(rule, ruleParams); + + return ruleFunc; + } + + /// + /// Builds the nested expression. + /// + /// The parent rule. + /// The child rules. + /// The operation. + /// The type parameter expressions. + /// The rule input exp. + /// Expression of func delegate + /// + private RuleFunc BuildNestedRuleFunc(Rule parentRule, ExpressionType operation, RuleParameter[] ruleParams) + { + var ruleFuncList = new List>(); + foreach (var r in parentRule.Rules) + { + ruleFuncList.Add(GetDelegateForRule(r, ruleParams)); + } + + return (paramArray) => + { + var resultList = ruleFuncList.Select(fn => fn(paramArray)); + Func isSuccess = (p) => ApplyOperation(resultList, operation); + RuleFunc result = Helpers.ToResultTree(parentRule, resultList,isSuccess); + return result(paramArray); + }; + } + + + private bool ApplyOperation(IEnumerable ruleResults, ExpressionType operation) + { + switch (operation) + { + case ExpressionType.And: + case ExpressionType.AndAlso: + return ruleResults.All(r => r.IsSuccess); + + case ExpressionType.Or: + case ExpressionType.OrElse: + return ruleResults.Any(r => r.IsSuccess); + default: + return false; + } + } + } +} diff --git a/src/RulesEngine/RulesEngine/RuleExpressionBuilderFactory.cs b/src/RulesEngine/RuleExpressionBuilderFactory.cs similarity index 75% rename from src/RulesEngine/RulesEngine/RuleExpressionBuilderFactory.cs rename to src/RulesEngine/RuleExpressionBuilderFactory.cs index d5480a6..5193943 100644 --- a/src/RulesEngine/RulesEngine/RuleExpressionBuilderFactory.cs +++ b/src/RulesEngine/RuleExpressionBuilderFactory.cs @@ -10,16 +10,18 @@ namespace RulesEngine internal class RuleExpressionBuilderFactory { private ReSettings _reSettings; - public RuleExpressionBuilderFactory(ReSettings reSettings) + private LambdaExpressionBuilder _lambdaExpressionBuilder; + public RuleExpressionBuilderFactory(ReSettings reSettings, RuleExpressionParser expressionParser) { _reSettings = reSettings; + _lambdaExpressionBuilder = new LambdaExpressionBuilder(_reSettings, expressionParser); } public RuleExpressionBuilderBase RuleGetExpressionBuilder(RuleExpressionType ruleExpressionType) { switch (ruleExpressionType) { case RuleExpressionType.LambdaExpression: - return new LambdaExpressionBuilder(_reSettings); + return _lambdaExpressionBuilder; default: throw new InvalidOperationException($"{nameof(ruleExpressionType)} has not been supported yet."); } diff --git a/src/RulesEngine/RulesEngine/RulesCache.cs b/src/RulesEngine/RulesCache.cs similarity index 88% rename from src/RulesEngine/RulesEngine/RulesCache.cs rename to src/RulesEngine/RulesCache.cs index 3853e4d..755dd3b 100644 --- a/src/RulesEngine/RulesEngine/RulesCache.cs +++ b/src/RulesEngine/RulesCache.cs @@ -13,7 +13,7 @@ namespace RulesEngine internal class RulesCache { /// The compile rules - private ConcurrentDictionary> _compileRules = new ConcurrentDictionary>(); + private ConcurrentDictionary>> _compileRules = new ConcurrentDictionary>>(); /// The workflow rules private ConcurrentDictionary _workflowRules = new ConcurrentDictionary(); @@ -47,7 +47,7 @@ namespace RulesEngine /// Adds the or update compiled rule. /// The compiled rule key. /// The compiled rule. - public void AddOrUpdateCompiledRule(string compiledRuleKey, IEnumerable compiledRule) + public void AddOrUpdateCompiledRule(string compiledRuleKey, IDictionary> compiledRule) { _compileRules.AddOrUpdate(compiledRuleKey, compiledRule, (k, v) => compiledRule); } @@ -64,6 +64,8 @@ namespace RulesEngine /// IEnumerable<Rule>. public IEnumerable GetRules(string workflowName) { + if (!ContainsWorkflowRules(workflowName)) + throw new ArgumentException($"workflow `{workflowName}` was not found"); return _workflowRules[workflowName].Rules; } @@ -98,12 +100,12 @@ namespace RulesEngine return workflowRules; } } - + /// Gets the compiled rules. /// The compiled rules key. /// CompiledRule. - public IEnumerable GetCompiledRules(string compiledRulesKey) + public IDictionary> GetCompiledRules(string compiledRulesKey) { return _compileRules[compiledRulesKey]; } @@ -117,7 +119,7 @@ namespace RulesEngine var compiledKeysToRemove = _compileRules.Keys.Where(key => key.StartsWith(workflowName)); foreach (var key in compiledKeysToRemove) { - _compileRules.TryRemove(key, out IEnumerable val); + _compileRules.TryRemove(key, out IDictionary> val); } } } diff --git a/src/RulesEngine/RulesEngine.cs b/src/RulesEngine/RulesEngine.cs new file mode 100644 index 0000000..6be4e0a --- /dev/null +++ b/src/RulesEngine/RulesEngine.cs @@ -0,0 +1,397 @@ +using System.Threading.Tasks; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using FluentValidation; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using RulesEngine.Actions; +using RulesEngine.Enums; +using RulesEngine.Exceptions; +using RulesEngine.Interfaces; +using RulesEngine.Models; +using RulesEngine.Validators; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using RulesEngine.ExpressionBuilders; + +namespace RulesEngine +{ + /// + /// + /// + /// + public class RulesEngine : IRulesEngine + { + #region Variables + private readonly ILogger _logger; + private readonly ReSettings _reSettings; + private readonly RulesCache _rulesCache = new RulesCache(); + private readonly MemoryCache _compiledParamsCache = new MemoryCache(new MemoryCacheOptions()); + private readonly ParamCompiler _ruleParamCompiler; + 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, ReSettings reSettings = null) : this(logger, reSettings) + { + var workflowRules = jsonConfig.Select(item => JsonConvert.DeserializeObject(item)).ToArray(); + AddWorkflow(workflowRules); + } + + public RulesEngine(WorkflowRules[] workflowRules, ILogger logger = null, ReSettings reSettings = null) : this(logger, reSettings) + { + AddWorkflow(workflowRules); + } + + public RulesEngine(ILogger logger = null, ReSettings reSettings = null) + { + _logger = logger ?? new NullLogger(); + _reSettings = reSettings ?? new ReSettings(); + _ruleExpressionParser = new RuleExpressionParser(_reSettings); + _ruleParamCompiler = new ParamCompiler(_reSettings, _ruleExpressionParser); + _ruleCompiler = new RuleCompiler(new RuleExpressionBuilderFactory(_reSettings, _ruleExpressionParser),_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 (int 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); + foreach(var ruleResult in ruleResultList){ + var actionResult = await ExecuteActionForRuleResult(ruleResult,false); + ruleResult.ActionResult = new ActionResult{ + Output = actionResult.Output, + Exception = actionResult.Exception + }; + } + return ruleResultList; + } + + + 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) + { + ActionTriggerType triggerType = resultTree?.IsSuccess == true ? ActionTriggerType.onSuccess : ActionTriggerType.onFailure; + + if (resultTree?.Rule?.Actions != null && resultTree.Rule.Actions.ContainsKey(triggerType)) + { + var actionInfo = resultTree.Rule.Actions[triggerType]; + 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. + /// + /// The workflow rules. + /// + public void AddWorkflow(params WorkflowRules[] workflowRules) + { + try + { + foreach (var workflowRule in workflowRules) + { + var validator = new WorkflowRulesValidator(); + validator.ValidateAndThrow(workflowRule); + _rulesCache.AddOrUpdateWorkflowRules(workflowRule.WorkflowName, workflowRule); + } + } + catch (ValidationException ex) + { + throw new RuleValidationException(ex.Message, ex.Errors); + } + } + + /// + /// Clears the workflows. + /// + public void ClearWorkflows() + { + _rulesCache.Clear(); + } + + /// + /// Removes the workflow. + /// + /// The workflow names. + public void RemoveWorkflow(params string[] workflowNames) + { + foreach (var workflowName in workflowNames) + { + _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) + { + string compileRulesKey = GetCompiledRulesKey(workflowName,ruleParams); + if (_rulesCache.ContainsCompiledRules(compileRulesKey)) + return true; + + var workflowRules = _rulesCache.GetWorkFlowRules(workflowName); + if (workflowRules != null) + { + var dictFunc = new Dictionary>(); + foreach (var rule in _rulesCache.GetRules(workflowName)) + { + dictFunc.Add(rule.RuleName,CompileRule(workflowName, ruleParams, rule)); + } + + _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 rules = _rulesCache.GetRules(workflowName); + var currentRule = rules?.SingleOrDefault(c => c.RuleName == ruleName); + if(currentRule == null){ + throw new ArgumentException($"Workflow `{workflowName}` does not contain any rule named `{ruleName}`"); + } + return CompileRule(workflowName,ruleParameters,currentRule); + } + + private RuleFunc CompileRule(string workflowName, RuleParameter[] ruleParams, Rule rule) + { + var compiledParamsKey = GetCompiledParamsCacheKey(workflowName, rule.RuleName, ruleParams); + IEnumerable compiledParamList = _compiledParamsCache.GetOrCreate(compiledParamsKey, (entry) => _ruleParamCompiler.CompileParamsExpression(rule, ruleParams)); + var compiledRuleParameters = compiledParamList?.Select(c => c.AsRuleParameter()) ?? new List(); + var updatedRuleParams = ruleParams?.Concat(compiledRuleParameters); + var compiledRule = _ruleCompiler.CompileRule(rule, updatedRuleParams?.ToArray()); + + RuleFunc updatedRule = (RuleParameter[] paramList) => + { + var inputs = paramList.AsEnumerable(); + IEnumerable localParams = compiledParamList ?? new List(); + var evaluatedParamList = new List(); + foreach (var localParam in localParams) + { + var evaluatedLocalParam = _ruleParamCompiler.EvaluateCompiledParam(localParam.Name, localParam.Value, inputs); + inputs = inputs.Append(evaluatedLocalParam); + evaluatedParamList.Add(evaluatedLocalParam); + } + var result = compiledRule(inputs.ToArray()); + result.RuleEvaluatedParams = evaluatedParamList; + return result; + }; + return updatedRule; + } + + + + /// + /// 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"); + + List result = new List(); + string 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 string GetCompiledParamsCacheKey(string workflowName,string ruleName,RuleParameter[] ruleParams) + { + var key = $"compiledparams-{workflowName}-{ruleName}" + String.Join("-", ruleParams.Select(c => c.Type.Name)); + return key; + } + + private IDictionary> GetDefaultActionRegistry(){ + return new Dictionary>{ + {"OutputExpression",() => new OutputExpressionAction(_ruleExpressionParser) }, + {"EvaluateRule", () => new EvaluateRuleAction(this) } + }; + } + + /// + /// The result + /// + /// The result. + /// Updated error message. + private IEnumerable FormatErrorMessages(IEnumerable ruleResultList) + { + + foreach (var ruleResult in ruleResultList?.Where(r => !r.IsSuccess)) + { + var errorMessage = ruleResult?.Rule?.ErrorMessage; + if(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/RulesEngine.csproj b/src/RulesEngine/RulesEngine.csproj similarity index 77% rename from src/RulesEngine/RulesEngine/RulesEngine.csproj rename to src/RulesEngine/RulesEngine.csproj index 5694db3..6a08b84 100644 --- a/src/RulesEngine/RulesEngine/RulesEngine.csproj +++ b/src/RulesEngine/RulesEngine.csproj @@ -1,8 +1,8 @@ - + netstandard2.0 - 2.1.4 + 3.0.0-preview.1 Copyright (c) Microsoft Corporation. LICENSE https://github.com/microsoft/RulesEngine @@ -14,30 +14,25 @@ true - true true snupkg + + + + + + + + + + + + - - - - - - - - - - - - True - - - - diff --git a/src/RulesEngine/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs b/src/RulesEngine/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs deleted file mode 100644 index 8db695f..0000000 --- a/src/RulesEngine/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using RulesEngine; -using RulesEngine.ExpressionBuilders; -using RulesEngine.HelperFunctions; -using RulesEngine.Models; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Dynamic.Core; -using System.Linq.Expressions; - -namespace RulesEngine.ExpressionBuilders -{ - /// - /// This class will build the list expression - /// - internal sealed class LambdaExpressionBuilder : RuleExpressionBuilderBase - { - private readonly ReSettings _reSettings; - - internal LambdaExpressionBuilder(ReSettings reSettings) - { - _reSettings = reSettings; - } - - internal override Expression> BuildExpressionForRule(Rule rule, IEnumerable typeParamExpressions, ParameterExpression ruleInputExp) - { - try - { - var config = new ParsingConfig { CustomTypeProvider = new CustomTypeProvider(_reSettings.CustomTypes) }; - var e = DynamicExpressionParser.ParseLambda(config, typeParamExpressions.ToArray(), null, rule.Expression); - var body = e.Body is BinaryExpression binaryExpression - ? binaryExpression - : Expression.MakeBinary(ExpressionType.And, e.Body, Expression.Constant(true)); - return Helpers.ToResultTreeExpression(rule, null, body, typeParamExpressions, ruleInputExp); - } - catch (Exception ex) - { - ex.Data.Add(nameof(rule.RuleName), rule.RuleName); - ex.Data.Add(nameof(rule.Expression), rule.Expression); - - if (!_reSettings.EnableExceptionAsErrorMessage) throw; - - var binaryExpression = Expression.And(Expression.Constant(true), Expression.Constant(false)); - var exceptionMessage = ex.Message; - return Helpers.ToResultTreeExpression(rule, null, binaryExpression, typeParamExpressions, ruleInputExp, exceptionMessage); - } - } - - /// Builds the expression for rule parameter. - /// The parameter. - /// The type parameter expressions. - /// The rule input exp. - /// Expression. - internal override Expression BuildExpressionForRuleParam(LocalParam param, IEnumerable typeParamExpressions, ParameterExpression ruleInputExp) - { - var config = new ParsingConfig { CustomTypeProvider = new CustomTypeProvider(_reSettings.CustomTypes) }; - var e = DynamicExpressionParser.ParseLambda(config, typeParamExpressions.ToArray(), null, param.Expression); - return e.Body; - } - } -} \ No newline at end of file diff --git a/src/RulesEngine/RulesEngine/HelperFunctions/Helpers.cs b/src/RulesEngine/RulesEngine/HelperFunctions/Helpers.cs deleted file mode 100644 index 0446b44..0000000 --- a/src/RulesEngine/RulesEngine/HelperFunctions/Helpers.cs +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using RulesEngine.Models; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; - -namespace RulesEngine.HelperFunctions -{ - /// - /// Helpers - /// - internal static class Helpers - { - /// - /// To the result tree expression. - /// - /// The rule. - /// The child rule results. - /// The is success exp. - /// The type parameter expressions. - /// The rule input exp. - /// Expression of func - internal static Expression> ToResultTreeExpression(Rule rule, IEnumerable childRuleResults, BinaryExpression isSuccessExp, IEnumerable typeParamExpressions, ParameterExpression ruleInputExp, string exceptionMessage = "") - { - var memberInit = ToResultTree(rule, childRuleResults, isSuccessExp, typeParamExpressions, null, exceptionMessage); - var lambda = Expression.Lambda>(memberInit, new[] { ruleInputExp }); - return lambda; - } - - /// - /// To the result tree member expression - /// - /// The rule. - /// The child rule results. - /// The is success exp. - /// The child rule results block expression. - /// - internal static MemberInitExpression ToResultTree(Rule rule, IEnumerable childRuleResults, BinaryExpression isSuccessExp, IEnumerable typeParamExpressions, BlockExpression childRuleResultsblockexpr, string exceptionMessage = "") - { - var createdType = typeof(RuleResultTree); - var ctor = Expression.New(createdType); - - var ruleProp = createdType.GetProperty(nameof(RuleResultTree.Rule)); - var isSuccessProp = createdType.GetProperty(nameof(RuleResultTree.IsSuccess)); - var childResultProp = createdType.GetProperty(nameof(RuleResultTree.ChildResults)); - var inputProp = createdType.GetProperty(nameof(RuleResultTree.Input)); - var exceptionProp = createdType.GetProperty(nameof(RuleResultTree.ExceptionMessage)); - - var rulePropBinding = Expression.Bind(ruleProp, Expression.Constant(rule)); - var isSuccessPropBinding = Expression.Bind(isSuccessProp, isSuccessExp); - var inputBinding = Expression.Bind(inputProp, typeParamExpressions.FirstOrDefault()); - var exceptionBinding = Expression.Bind(exceptionProp, Expression.Constant(exceptionMessage)); - - MemberInitExpression memberInit; - - if (childRuleResults != null) - { - var ruleResultTreeArr = Expression.NewArrayInit(typeof(RuleResultTree), childRuleResults); - - var childResultPropBinding = Expression.Bind(childResultProp, ruleResultTreeArr); - memberInit = Expression.MemberInit(ctor, new[] { rulePropBinding, isSuccessPropBinding, childResultPropBinding, inputBinding, exceptionBinding }); - } - else if (childRuleResultsblockexpr != null) - { - var childResultPropBinding = Expression.Bind(childResultProp, childRuleResultsblockexpr); - memberInit = Expression.MemberInit(ctor, new[] { rulePropBinding, isSuccessPropBinding, childResultPropBinding, inputBinding, exceptionBinding }); - } - else - { - memberInit = Expression.MemberInit(ctor, new[] { rulePropBinding, isSuccessPropBinding, inputBinding, exceptionBinding }); - } - - return memberInit; - } - - /// - /// To the result tree error messages - /// - /// ruleResultTree - /// ruleResultMessage - internal static void ToResultTreeMessages(RuleResultTree ruleResultTree, ref RuleResultMessage ruleResultMessage) - { - if (ruleResultTree.ChildResults != null) - { - GetChildRuleMessages(ruleResultTree.ChildResults, ref ruleResultMessage); - } - else - { - if (ruleResultTree.IsSuccess) - { - string errMsg = ruleResultTree.Rule.ErrorMessage; - errMsg = string.IsNullOrEmpty(errMsg) ? $"Error message does not configured for {ruleResultTree.Rule.RuleName}" : errMsg; - - if (ruleResultTree.Rule.ErrorType == ErrorType.Error && !ruleResultMessage.ErrorMessages.Contains(errMsg)) - { - ruleResultMessage.ErrorMessages.Add(errMsg); - } - else if (ruleResultTree.Rule.ErrorType == ErrorType.Warning && !ruleResultMessage.WarningMessages.Contains(errMsg)) - { - ruleResultMessage.WarningMessages.Add(errMsg); - } - } - } - } - - /// - /// To get the child error message recersivly - /// - /// childResultTree - /// ruleResultMessage - private static void GetChildRuleMessages(IEnumerable childResultTree, ref RuleResultMessage ruleResultMessage) - { - foreach (var item in childResultTree) - { - ToResultTreeMessages(item, ref ruleResultMessage); - } - } - } -} diff --git a/src/RulesEngine/RulesEngine/Models/CompiledRule.cs b/src/RulesEngine/RulesEngine/Models/CompiledRule.cs deleted file mode 100644 index f1d7959..0000000 --- a/src/RulesEngine/RulesEngine/Models/CompiledRule.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace RulesEngine.Models -{ - [ExcludeFromCodeCoverage] - internal class CompiledRule - { - /// - /// Gets or sets the compiled rules. - /// - /// - /// The compiled rules. - /// - internal Delegate Rule { get; set; } - - - /// - /// Gets or sets the rule parameters. - /// - /// - /// The rule parameters. - /// - internal CompiledRuleParam CompiledParameters { get; set; } - } - -} diff --git a/src/RulesEngine/RulesEngine/Models/CompiledRuleParam.cs b/src/RulesEngine/RulesEngine/Models/CompiledRuleParam.cs deleted file mode 100644 index cecd2d5..0000000 --- a/src/RulesEngine/RulesEngine/Models/CompiledRuleParam.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace RulesEngine.Models -{ - /// Class CompiledRule. - internal class CompiledRuleParam - { - /// - /// Gets or sets the compiled rules. - /// - /// - /// The compiled rules. - /// - internal string Name { get; set; } - - /// Gets or sets the rule parameters. - /// The rule parameters. - internal IEnumerable CompiledParameters { get; set; } - - /// - /// Gets or sets the rule parameters. - /// - /// - /// The rule parameters. - /// - internal IEnumerable RuleParameters { get; set; } - } -} diff --git a/src/RulesEngine/RulesEngine/Models/Rule.cs b/src/RulesEngine/RulesEngine/Models/Rule.cs deleted file mode 100644 index f0178fa..0000000 --- a/src/RulesEngine/RulesEngine/Models/Rule.cs +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; - -namespace RulesEngine.Models -{ - /// - /// Rule class - /// - [ExcludeFromCodeCoverage] - public class Rule - { - /// - /// Gets or sets the name of the rule. - /// - /// - /// The name of the rule. - /// - public string RuleName { get; set; } - - /// - /// Gets or sets the operator. - /// - /// - /// The operator. - /// - public string Operator { get; set; } - - /// - /// Gets or sets the error message. - /// - /// - /// The error message. - /// - public string ErrorMessage { get; set; } - - /// - /// Gets or sets the type of the error. - /// - /// - /// The type of the error. - /// - [JsonConverter(typeof(StringEnumConverter))] - public ErrorType ErrorType { get; set; } - - /// - /// Gets or sets the type of the rule expression. - /// - /// - /// The type of the rule expression. - /// - [JsonConverter(typeof(StringEnumConverter))] - public RuleExpressionType? RuleExpressionType { get; set; } - - - /// - /// Gets or sets the names of common workflows - /// - public List WorkflowRulesToInject { get; set; } - - /// - /// Gets or sets the rules. - /// - /// - /// The rules. - /// - public List Rules { get; set; } - - /// - /// Gets the parameters. - /// - /// - /// The parameters. - /// - [JsonProperty] - public IEnumerable LocalParams { get; private set; } - - /// - /// Gets or Sets the lambda expression. - /// - public string Expression { get; set; } - - - /// - /// Gets or sets the success event. - /// - /// - /// The success event. - /// - public string SuccessEvent { get; set; } - - } - -} diff --git a/src/RulesEngine/RulesEngine/Models/RuleInput.cs b/src/RulesEngine/RulesEngine/Models/RuleInput.cs deleted file mode 100644 index 175533f..0000000 --- a/src/RulesEngine/RulesEngine/Models/RuleInput.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace RulesEngine.Models -{ - /// - /// Rule input - /// - [ExcludeFromCodeCoverage] - internal class RuleInput - { - /// - /// Gets the today UTC. - /// - /// - /// The today UTC. - /// - public DateTime TodayUtc { get; set; } - } -} diff --git a/src/RulesEngine/RulesEngine/ParamCache.cs b/src/RulesEngine/RulesEngine/ParamCache.cs deleted file mode 100644 index ab35fb0..0000000 --- a/src/RulesEngine/RulesEngine/ParamCache.cs +++ /dev/null @@ -1,65 +0,0 @@ -using RulesEngine.Models; -using System; -using System.Collections.Concurrent; -using System.Linq; - -namespace RulesEngine -{ - /// Maintains the cache of evaludated param. - internal class ParamCache where T : class - { - /// - /// The compile rules - /// - private readonly ConcurrentDictionary _evaluatedParams = new ConcurrentDictionary(); - - /// - /// - /// Determines whether the specified parameter key name contains parameters. - /// - /// - /// Name of the parameter key. - /// - /// true if the specified parameter key name contains parameters; otherwise, false. - public bool ContainsParams(string paramKeyName) - { - return _evaluatedParams.ContainsKey(paramKeyName); - } - - /// Adds the or update evaluated parameter. - /// Name of the parameter key. - /// The rule parameters. - public void AddOrUpdateParams(string paramKeyName, T ruleParameters) - { - _evaluatedParams.AddOrUpdate(paramKeyName, ruleParameters, (k, v) => v); - } - - /// Clears this instance. - public void Clear() - { - _evaluatedParams.Clear(); - } - - /// Gets the evaluated parameters. - /// Name of the parameter key. - /// Delegate[]. - public T GetParams(string paramKeyName) - { - return _evaluatedParams[paramKeyName]; - } - - /// Removes the specified workflow name. - /// Name of the workflow. - public void RemoveCompiledParams(string paramKeyName) - { - if (_evaluatedParams.TryRemove(paramKeyName, out T ruleParameters)) - { - var compiledKeysToRemove = _evaluatedParams.Keys.Where(key => key.StartsWith(paramKeyName)); - foreach (var key in compiledKeysToRemove) - { - _evaluatedParams.TryRemove(key, out T val); - } - } - } - } -} diff --git a/src/RulesEngine/RulesEngine/ParamCompiler.cs b/src/RulesEngine/RulesEngine/ParamCompiler.cs deleted file mode 100644 index 03e7689..0000000 --- a/src/RulesEngine/RulesEngine/ParamCompiler.cs +++ /dev/null @@ -1,148 +0,0 @@ -using Microsoft.Extensions.Logging; -using RulesEngine.Models; -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Text; -using System.Linq; - -namespace RulesEngine -{ - /// - /// Rule param compilers - /// - internal class ParamCompiler - { - /// - /// The expression builder factory - /// - private readonly RuleExpressionBuilderFactory _expressionBuilderFactory; - - /// - /// The logger - /// - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// The expression builder factory. - /// expressionBuilderFactory - internal ParamCompiler(RuleExpressionBuilderFactory expressionBuilderFactory, ILogger logger) - { - if (expressionBuilderFactory == null) - { - throw new ArgumentNullException($"{nameof(expressionBuilderFactory)} can't be null."); - } - - if (logger == null) - { - throw new ArgumentNullException($"{nameof(logger)} can't be null."); - } - - _logger = logger; - _expressionBuilderFactory = expressionBuilderFactory; - } - - /// - /// Compiles the and evaluate parameter expression. - /// - /// The rule. - /// The rule parameters. - /// - /// IEnumerable<RuleParameter>. - /// - public CompiledRuleParam CompileParamsExpression(Rule rule, IEnumerable ruleParams) - { - - CompiledRuleParam compiledRuleParam = null; - - if (rule.LocalParams != null) - { - var compiledParameters = new List(); - var evaluatedParameters = new List(); - foreach (var param in rule.LocalParams) - { - IEnumerable typeParameterExpressions = GetParameterExpression(ruleParams.ToArray()).ToList(); // calling ToList to avoid multiple calls this the method for nested rule scenario. - ParameterExpression ruleInputExp = Expression.Parameter(typeof(RuleInput), nameof(RuleInput)); - var ruleParamExpression = GetExpressionForRuleParam(param, typeParameterExpressions, ruleInputExp); - var lambdaParameterExps = new List(typeParameterExpressions) { ruleInputExp }; - var expression = Expression.Lambda(ruleParamExpression, lambdaParameterExps); - var compiledParam = expression.Compile(); - compiledParameters.Add(new CompiledParam { Name = param.Name, Value = compiledParam, Parameters = evaluatedParameters }); - var evaluatedParam = this.EvaluateCompiledParam(param.Name, compiledParam, ruleParams); - ruleParams = ruleParams.Concat(new List { evaluatedParam }); - evaluatedParameters.Add(evaluatedParam); - } - - compiledRuleParam = new CompiledRuleParam { Name = rule.RuleName, CompiledParameters = compiledParameters, RuleParameters = evaluatedParameters }; - } - - return compiledRuleParam; - } - - /// Evaluates the compiled parameter. - /// Name of the parameter. - /// The compiled parameter. - /// The rule parameters. - /// RuleParameter. - public RuleParameter EvaluateCompiledParam(string paramName, Delegate compiledParam, IEnumerable ruleParams) - { - var inputs = ruleParams.Select(c => c.Value); - var result = compiledParam.DynamicInvoke(new List(inputs) { new RuleInput() }.ToArray()); - return new RuleParameter(paramName, result); - } - - // - /// Gets the parameter expression. - /// - /// The types. - /// - /// - /// types - /// or - /// type - /// - private IEnumerable GetParameterExpression(params RuleParameter[] ruleParams) - { - foreach (var ruleParam in ruleParams) - { - if (ruleParam == null) - { - throw new ArgumentException($"{nameof(ruleParam)} can't be null."); - } - - yield return Expression.Parameter(ruleParam.Type, ruleParam.Name); - } - } - - /// - /// Gets the expression for rule. - /// - /// The rule. - /// The type parameter expressions. - /// The rule input exp. - /// - private Expression GetExpressionForRuleParam(LocalParam param, IEnumerable typeParameterExpressions, ParameterExpression ruleInputExp) - { - return BuildExpression(param, typeParameterExpressions, ruleInputExp); - } - - /// - /// Builds the expression. - /// - /// The rule. - /// The type parameter expressions. - /// The rule input exp. - /// - /// - private Expression BuildExpression(LocalParam param, IEnumerable typeParameterExpressions, ParameterExpression ruleInputExp) - { - var ruleExpressionBuilder = _expressionBuilderFactory.RuleGetExpressionBuilder(RuleExpressionType.LambdaExpression); - - var expression = ruleExpressionBuilder.BuildExpressionForRuleParam(param, typeParameterExpressions, ruleInputExp); - - return expression; - } - } -} diff --git a/src/RulesEngine/RulesEngine/RuleCompiler.cs b/src/RulesEngine/RulesEngine/RuleCompiler.cs deleted file mode 100644 index 5912647..0000000 --- a/src/RulesEngine/RulesEngine/RuleCompiler.cs +++ /dev/null @@ -1,270 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.Extensions.Logging; -using RulesEngine.HelperFunctions; -using RulesEngine.Models; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; - -namespace RulesEngine -{ - /// - /// Rule compilers - /// - internal class RuleCompiler - { - /// - /// The nested operators - /// - private readonly ExpressionType[] nestedOperators = new ExpressionType[] { ExpressionType.And, ExpressionType.AndAlso, ExpressionType.Or, ExpressionType.OrElse }; - - /// - /// The expression builder factory - /// - private readonly RuleExpressionBuilderFactory _expressionBuilderFactory; - - /// - /// The logger - /// - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// The expression builder factory. - /// expressionBuilderFactory - internal RuleCompiler(RuleExpressionBuilderFactory expressionBuilderFactory, ILogger logger) - { - if (expressionBuilderFactory == null) - { - throw new ArgumentNullException($"{nameof(expressionBuilderFactory)} can't be null."); - } - - if (logger == null) - { - throw new ArgumentNullException($"{nameof(logger)} can't be null."); - } - - _logger = logger; - _expressionBuilderFactory = expressionBuilderFactory; - } - - /// - /// Compiles the rule - /// - /// - /// - /// - /// - /// Compiled func delegate - public Delegate CompileRule(Rule rule,params RuleParameter[] ruleParams) - { - try - { - IEnumerable typeParameterExpressions = GetParameterExpression(ruleParams).ToList(); // calling ToList to avoid multiple calls this the method for nested rule scenario. - - ParameterExpression ruleInputExp = Expression.Parameter(typeof(RuleInput), nameof(RuleInput)); - - Expression> ruleExpression = GetExpressionForRule(rule, typeParameterExpressions, ruleInputExp); - - var lambdaParameterExps = new List(typeParameterExpressions) { ruleInputExp }; - - - var expression = Expression.Lambda(ruleExpression.Body, lambdaParameterExps); - - - return expression.Compile(); - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - throw; - } - } - - // - /// Gets the parameter expression. - /// - /// The types. - /// - /// - /// types - /// or - /// type - /// - private IEnumerable GetParameterExpression(params RuleParameter[] ruleParams) - { - if (ruleParams == null || !ruleParams.Any()) - { - throw new ArgumentException($"{nameof(ruleParams)} can't be null/empty."); - } - - foreach (var ruleParam in ruleParams) - { - if (ruleParam == null) - { - throw new ArgumentException($"{nameof(ruleParam)} can't be null."); - } - - yield return Expression.Parameter(ruleParam.Type, ruleParam.Name); - } - } - - /// - /// Gets the expression for rule. - /// - /// The rule. - /// The type parameter expressions. - /// The rule input exp. - /// - private Expression> GetExpressionForRule(Rule rule, IEnumerable typeParameterExpressions, ParameterExpression ruleInputExp) - { - ExpressionType nestedOperator; - - if (Enum.TryParse(rule.Operator, out nestedOperator) && nestedOperators.Contains(nestedOperator) && - rule.Rules != null && rule.Rules.Any()) - { - return BuildNestedExpression(rule, nestedOperator, typeParameterExpressions, ruleInputExp); - } - else - { - return BuildExpression(rule, typeParameterExpressions, ruleInputExp); - } - } - - /// - /// Builds the expression. - /// - /// The rule. - /// The type parameter expressions. - /// The rule input exp. - /// - /// - private Expression> BuildExpression(Rule rule, IEnumerable typeParameterExpressions, ParameterExpression ruleInputExp) - { - if (!rule.RuleExpressionType.HasValue) - { - throw new InvalidOperationException($"RuleExpressionType can not be null for leaf level expressions."); - } - - var ruleExpressionBuilder = _expressionBuilderFactory.RuleGetExpressionBuilder(rule.RuleExpressionType.Value); - - var expression = ruleExpressionBuilder.BuildExpressionForRule(rule, typeParameterExpressions, ruleInputExp); - - return expression; - } - - /// - /// Builds the nested expression. - /// - /// The parent rule. - /// The child rules. - /// The operation. - /// The type parameter expressions. - /// The rule input exp. - /// Expression of func delegate - /// - private Expression> BuildNestedExpression(Rule parentRule, ExpressionType operation, IEnumerable typeParameterExpressions, ParameterExpression ruleInputExp) - { - List>> expressions = new List>>(); - foreach (var r in parentRule.Rules) - { - expressions.Add(GetExpressionForRule(r, typeParameterExpressions, ruleInputExp)); - } - - List childRuleResultTree = new List(); - - foreach (var exp in expressions) - { - var resultMemberInitExpression = exp.Body as MemberInitExpression; - - if (resultMemberInitExpression == null)// assert is a MemberInitExpression - { - throw new InvalidCastException($"expression.Body '{exp.Body}' is not of MemberInitExpression type."); - } - - childRuleResultTree.Add(resultMemberInitExpression); - } - - Expression> nestedExpression = Helpers.ToResultTreeExpression(parentRule, childRuleResultTree, BinaryExpression(expressions, operation), typeParameterExpressions, ruleInputExp); - - return nestedExpression; - } - - /// - /// Binaries the expression. - /// - /// The expressions. - /// Type of the operation. - /// Binary Expression - private BinaryExpression BinaryExpression(IList>> expressions, ExpressionType operationType) - { - if (expressions.Count == 1) - { - return ResolveIsSuccessBinding(expressions.First()); - } - - BinaryExpression nestedBinaryExp = Expression.MakeBinary(operationType, ResolveIsSuccessBinding(expressions[0]), ResolveIsSuccessBinding(expressions[1])); - - for (int i = 2; expressions.Count > i; i++) - { - nestedBinaryExp = Expression.MakeBinary(operationType, nestedBinaryExp, ResolveIsSuccessBinding(expressions[i])); - } - - return nestedBinaryExp; - } - - /// - /// Resolves the is success binding. - /// - /// The expression. - /// Binary expression of IsSuccess prop - /// expression - /// - /// - /// IsSuccess - /// or - /// IsSuccess - /// or - /// IsSuccess - /// - private BinaryExpression ResolveIsSuccessBinding(Expression> expression) - { - if (expression == null) - { - throw new ArgumentNullException($"{nameof(expression)} should not be null."); - } - - var memberInitExpression = expression.Body as MemberInitExpression; - - if (memberInitExpression == null)// assert it's a MemberInitExpression - { - throw new InvalidCastException($"expression.Body '{expression.Body}' is not of MemberInitExpression type."); - } - - MemberAssignment isSuccessBinding = (MemberAssignment)memberInitExpression.Bindings.FirstOrDefault(f => f.Member.Name == nameof(RuleResultTree.IsSuccess)); - - if (isSuccessBinding == null) - { - throw new NullReferenceException($"Expected {nameof(RuleResultTree.IsSuccess)} property binding not found in {memberInitExpression}."); - } - - if (isSuccessBinding.Expression == null) - { - throw new NullReferenceException($"{nameof(RuleResultTree.IsSuccess)} assignment expression can not be null."); - } - - BinaryExpression isSuccessExpression = isSuccessBinding.Expression as BinaryExpression; - - if (isSuccessExpression == null) - { - throw new NullReferenceException($"Expected {nameof(RuleResultTree.IsSuccess)} assignment expression to be of {typeof(BinaryExpression)} and not {isSuccessBinding.Expression.GetType()}"); - } - - return isSuccessExpression; - } - } -} diff --git a/src/RulesEngine/RulesEngine/RulesEngine.cs b/src/RulesEngine/RulesEngine/RulesEngine.cs deleted file mode 100644 index 8caaee1..0000000 --- a/src/RulesEngine/RulesEngine/RulesEngine.cs +++ /dev/null @@ -1,345 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using FluentValidation; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using RulesEngine.Exceptions; -using RulesEngine.HelperFunctions; -using RulesEngine.Interfaces; -using RulesEngine.Models; -using RulesEngine.Validators; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; - -namespace RulesEngine -{ - /// - /// - /// - /// - public class RulesEngine : IRulesEngine - { - #region Variables - - /// - /// The logger - /// - private readonly ILogger _logger; - - /// - /// The re settings - /// - private readonly ReSettings _reSettings; - - /// - /// The rules cache - /// - private readonly RulesCache _rulesCache = new RulesCache(); - - /// - /// The parameters cache - /// - private readonly ParamCache _compiledParamsCache = new ParamCache(); - - /// - /// The rule parameter compiler - /// - private readonly ParamCompiler ruleParamCompiler; - - /// - /// The parameter parse regex - /// - private const string ParamParseRegex = "(\\$\\(.*?\\))"; - #endregion - - #region Constructor - public RulesEngine(string[] jsonConfig, ILogger logger, ReSettings reSettings = null) : this(logger, reSettings) - { - var workflowRules = jsonConfig.Select(item => JsonConvert.DeserializeObject(item)).ToArray(); - AddWorkflow(workflowRules); - } - - public RulesEngine(WorkflowRules[] workflowRules, ILogger logger, ReSettings reSettings = null) : this(logger, reSettings) - { - AddWorkflow(workflowRules); - } - - public RulesEngine(ILogger logger, ReSettings reSettings = null) - { - _logger = logger ?? new NullLogger(); - _reSettings = reSettings ?? new ReSettings(); - ruleParamCompiler = new ParamCompiler(new RuleExpressionBuilderFactory(_reSettings), _logger); - } - #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 List ExecuteRule(string workflowName, params object[] inputs) - { - _logger.LogTrace($"Called ExecuteRule for workflow {workflowName} and count of input {inputs.Count()}"); - - var ruleParams = new List(); - - for (int i = 0; i < inputs.Length; i++) - { - var input = inputs[i]; - ruleParams.Add(new RuleParameter($"input{i + 1}", input)); - } - - return ExecuteRule(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 List ExecuteRule(string workflowName, params RuleParameter[] ruleParams) - { - return ValidateWorkflowAndExecuteRule(workflowName, ruleParams); - } - - #endregion - - #region Private Methods - - /// - /// Adds the workflow. - /// - /// The workflow rules. - /// - public void AddWorkflow(params WorkflowRules[] workflowRules) - { - try - { - foreach (var workflowRule in workflowRules) - { - var validator = new WorkflowRulesValidator(); - validator.ValidateAndThrow(workflowRule); - _rulesCache.AddOrUpdateWorkflowRules(workflowRule.WorkflowName, workflowRule); - } - } - catch (ValidationException ex) - { - throw new RuleValidationException(ex.Message, ex.Errors); - } - } - - /// - /// Clears the workflows. - /// - public void ClearWorkflows() - { - _rulesCache.Clear(); - } - - /// - /// Removes the workflow. - /// - /// The workflow names. - public void RemoveWorkflow(params string[] workflowNames) - { - foreach (var workflowName in workflowNames) - { - _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 (RegisterCompiledRule(workflowName, ruleParams)) - { - result = ExecuteRuleByWorkflow(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 RegisterCompiledRule(string workflowName, params RuleParameter[] ruleParams) - { - string compileRulesKey = GetCompiledRulesKey(workflowName,ruleParams); - if (_rulesCache.ContainsCompiledRules(compileRulesKey)) - return true; - - var workflowRules = _rulesCache.GetWorkFlowRules(workflowName); - if (workflowRules != null) - { - var lstFunc = new List(); - var ruleCompiler = new RuleCompiler(new RuleExpressionBuilderFactory(_reSettings), _logger); - foreach (var rule in _rulesCache.GetRules(workflowName)) - { - var compiledParamsKey = GetCompiledParamsCacheKey(workflowName, rule.RuleName, ruleParams); - CompiledRuleParam compiledRuleParam; - if (_compiledParamsCache.ContainsParams(compiledParamsKey)) - { - compiledRuleParam = _compiledParamsCache.GetParams(compiledParamsKey); - } - else - { - compiledRuleParam = ruleParamCompiler.CompileParamsExpression(rule, ruleParams); - _compiledParamsCache.AddOrUpdateParams(compiledParamsKey, compiledRuleParam); - } - - var updatedRuleParams = compiledRuleParam != null ? ruleParams?.Concat(compiledRuleParam?.RuleParameters) : ruleParams; - var compiledRule = ruleCompiler.CompileRule(rule, updatedRuleParams?.ToArray()); - lstFunc.Add(new CompiledRule { Rule = compiledRule, CompiledParameters = compiledRuleParam }); - } - - _rulesCache.AddOrUpdateCompiledRule(compileRulesKey, lstFunc); - _logger.LogTrace($"Rules has been compiled for the {workflowName} workflow and added to dictionary"); - return true; - } - else - { - return false; - } - } - private string GetCompiledRulesKey(string workflowName, RuleParameter[] ruleParams) - { - var key = $"{workflowName}-" + String.Join("-", ruleParams.Select(c => c.Type.Name)); - return key.GetHashCode().ToString(); - } - - private string GetCompiledParamsCacheKey(string workflowName,string ruleName,RuleParameter[] ruleParams) - { - var key = $"compiledparams-{workflowName}-{ruleName}" + String.Join("-", ruleParams.Select(c => c.Type.Name)); - return key.GetHashCode().ToString(); - } - - /// - /// This will execute the compiled rules - /// - /// - /// - /// list of rule result set - private List ExecuteRuleByWorkflow(string workflowName, RuleParameter[] ruleParameters) - { - _logger.LogTrace($"Compiled rules found for {workflowName} workflow and executed"); - - List result = new List(); - string compileRulesKey = GetCompiledRulesKey(workflowName,ruleParameters); - foreach (var compiledRule in _rulesCache.GetCompiledRules(compileRulesKey)) - { - IEnumerable evaluatedRuleParams = new List(ruleParameters); - if (compiledRule?.CompiledParameters?.CompiledParameters != null) - { - foreach (var compiledParam in compiledRule?.CompiledParameters?.CompiledParameters) - { - var evaluatedParam = ruleParamCompiler.EvaluateCompiledParam(compiledParam.Name, compiledParam.Value, evaluatedRuleParams); - evaluatedRuleParams = evaluatedRuleParams.Concat(new List { evaluatedParam }); - } - } - - var inputs = evaluatedRuleParams.Select(c => c.Value); - var resultTree = compiledRule.Rule.DynamicInvoke(new List(inputs) { new RuleInput() }.ToArray()) as RuleResultTree; - resultTree.RuleEvaluatedParams = evaluatedRuleParams; - result.Add(resultTree); - } - - FormatErrorMessages(result?.Where(r => !r.IsSuccess)); - return result; - } - - /// - /// The result - /// - /// The result. - /// Updated error message. - private IEnumerable FormatErrorMessages(IEnumerable result) - { - foreach (var error in result) - { - if (string.IsNullOrWhiteSpace(error?.Rule?.ErrorMessage)) - { - continue; - } - - var errorParameters = Regex.Matches(error?.Rule?.ErrorMessage, ParamParseRegex); - var errorMessage = error?.Rule?.ErrorMessage; - var evaluatedParams = error?.RuleEvaluatedParams; - foreach (var param in errorParameters) - { - var paramVal = param?.ToString(); - var property = paramVal?.Substring(2, paramVal.Length - 3); - if (property?.Split('.')?.Count() > 1) - { - var typeName = property?.Split('.')?[0]; - var propertyName = property?.Split('.')?[1]; - errorMessage = UpdateErrorMessage(errorMessage, evaluatedParams, property, typeName, propertyName); - } - else - { - var arrParams = evaluatedParams?.Select(c => new { c.Name, c.Value }); - var model = arrParams?.Where(a => string.Equals(a.Name, property))?.FirstOrDefault(); - var value = model?.Value != null ? JsonConvert.SerializeObject(model?.Value) : null; - errorMessage = errorMessage?.Replace($"$({property})", value ?? $"$({property})"); - } - } - - error.ExceptionMessage = errorMessage; - } - - return result; - } - - /// - /// Updates the error message. - /// - /// The error message. - /// The evaluated parameters. - /// The property. - /// Name of the type. - /// Name of the property. - /// Updated error message. - private static string UpdateErrorMessage(string errorMessage, IEnumerable evaluatedParams, string property, string typeName, string propertyName) - { - var arrParams = evaluatedParams?.Select(c => new { c.Name, c.Value }); - var model = arrParams?.Where(a => string.Equals(a.Name, typeName))?.FirstOrDefault(); - if (model != null) - { - var modelJson = JsonConvert.SerializeObject(model?.Value); - var jObj = JObject.Parse(modelJson); - JToken jToken = null; - var val = jObj?.TryGetValue(propertyName, StringComparison.OrdinalIgnoreCase, out jToken); - errorMessage = errorMessage.Replace($"$({property})", jToken != null ? jToken?.ToString() : $"({property})"); - } - - return errorMessage; - } - #endregion - } -} diff --git a/src/RulesEngine/RulesEngine/Validators/RuleValidator.cs b/src/RulesEngine/Validators/RuleValidator.cs similarity index 100% rename from src/RulesEngine/RulesEngine/Validators/RuleValidator.cs rename to src/RulesEngine/Validators/RuleValidator.cs diff --git a/src/RulesEngine/RulesEngine/Validators/WorkflowRulesValidator.cs b/src/RulesEngine/Validators/WorkflowRulesValidator.cs similarity index 100% rename from src/RulesEngine/RulesEngine/Validators/WorkflowRulesValidator.cs rename to src/RulesEngine/Validators/WorkflowRulesValidator.cs diff --git a/src/RulesEngine/global.json b/src/RulesEngine/global.json deleted file mode 100644 index d36fe28..0000000 --- a/src/RulesEngine/global.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "sdk": { - "version": "3.1", - "rollForward": "latestFeature", - "allowPrerelease": false - } -} \ No newline at end of file diff --git a/test/RulesEngine.UnitTest/ActionTests/ActionContextTests.cs b/test/RulesEngine.UnitTest/ActionTests/ActionContextTests.cs new file mode 100644 index 0000000..cb13cf0 --- /dev/null +++ b/test/RulesEngine.UnitTest/ActionTests/ActionContextTests.cs @@ -0,0 +1,183 @@ +using AutoFixture; +using Moq; +using RulesEngine.Actions; +using RulesEngine.Models; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Xunit; + +namespace RulesEngine.UnitTest +{ + [ExcludeFromCodeCoverage] + public class ActionContextTests + { + [Fact] + public void GetParentRuleResult_ReturnsParentRule() + { + // Arrange + var fixture = new Fixture(); + var contextInput = fixture.Create(); + var context = new Dictionary { + { nameof(contextInput), contextInput } + }; + var parentRuleResult = new RuleResultTree(); + + var actionContext = new ActionContext(context, parentRuleResult); + + // Act + var result = actionContext.GetParentRuleResult(); + + // Assert + Assert.NotNull(result); + Assert.Equal(parentRuleResult, result); + } + + [Fact] + public void GetContext_ValidName_ReturnsContext() + { + // Arrange + var fixture = new Fixture(); + var contextInput = fixture.Create(); + var context = new Dictionary { + { nameof(contextInput), contextInput } + }; + var parentRuleResult = new RuleResultTree(); + + var actionContext = new ActionContext(context, parentRuleResult); + string name = nameof(contextInput); + + // Act + var result = actionContext.GetContext(name); + + // Assert + Assert.Equal(contextInput, result); + } + + [Fact] + public void GetContext_ObjectContext_ReturnsTypedContext() + { + // Arrange + var fixture = new Fixture(); + var contextInput = fixture.CreateMany(); + var context = new Dictionary { + { nameof(contextInput), contextInput } + }; + var parentRuleResult = new RuleResultTree(); + + + var actionContext = new ActionContext(context, parentRuleResult); + string name = nameof(contextInput); + + // Act + var result = actionContext.GetContext>(name); + + // Assert + Assert.Equal(contextInput, result); + } + + [Fact] + public void GetContext_ValidNameWithStringCaseDiffernce_ReturnsContext() + { + // Arrange + var fixture = new Fixture(); + var contextInput = fixture.Create(); + var context = new Dictionary { + { nameof(contextInput), contextInput } + }; + var parentRuleResult = new RuleResultTree(); + + var actionContext = new ActionContext(context, parentRuleResult); + string name = nameof(contextInput).ToUpper(); + + // Act + var result = actionContext.GetContext(name); + + // Assert + Assert.Equal(contextInput, result); + } + + [Fact] + public void GetContext_InvalidName_ThrowsArgumentException() + { + // Arrange + var fixture = new Fixture(); + var contextInput = fixture.Create(); + var context = new Dictionary { + { nameof(contextInput), contextInput } + }; + var parentRuleResult = new RuleResultTree(); + + var actionContext = new ActionContext(context, parentRuleResult); + string name = fixture.Create(); + + // Act + Assert.Throws(() => actionContext.GetContext(name)); + } + + [Fact] + public void GetContext_PrimitiveInputs_ReturnsResult() + { + // Arrange + var fixture = new Fixture(); + var intInput = fixture.Create(); + var strInput = fixture.Create(); + var floatInput = fixture.Create(); + + var context = new Dictionary { + { nameof(intInput), intInput }, + { nameof(strInput), strInput }, + { nameof(floatInput), floatInput }, + }; + var parentRuleResult = new RuleResultTree(); + + var actionContext = new ActionContext(context, parentRuleResult); + + // Act + var intResult = actionContext.GetContext(nameof(intInput)); + var strResult = actionContext.GetContext(nameof(strInput)); + var floatResult = actionContext.GetContext(nameof(floatInput)); + + // Assert + Assert.Equal(intInput, intResult); + Assert.Equal(strInput, strResult); + Assert.Equal(floatInput, floatResult); + } + + [Fact] + public void GetContext_InvalidNameListContext_ThrowsArgumentException() + { + // Arrange + var fixture = new Fixture(); + var contextInput = fixture.CreateMany(); + var context = new Dictionary { + { nameof(contextInput), contextInput } + }; + var parentRuleResult = new RuleResultTree(); + + var actionContext = new ActionContext(context, parentRuleResult); + string name = fixture.Create(); + + // Act + Assert.Throws(() => actionContext.GetContext>(name)); + } + + [Fact] + public void GetContext_InvalidTypeConversion_ThrowsArgumentException() + { + // Arrange + var fixture = new Fixture(); + var contextInput = fixture.CreateMany(); + var context = new Dictionary { + { nameof(contextInput), contextInput } + }; + var parentRuleResult = new RuleResultTree(); + + var actionContext = new ActionContext(context, parentRuleResult); + string name = nameof(contextInput); + + // Act + Assert.Throws(() => actionContext.GetContext(name)); + } + } +} diff --git a/test/RulesEngine.UnitTest/ActionTests/RulesEngineWithActionsTests.cs b/test/RulesEngine.UnitTest/ActionTests/RulesEngineWithActionsTests.cs new file mode 100644 index 0000000..65d7f53 --- /dev/null +++ b/test/RulesEngine.UnitTest/ActionTests/RulesEngineWithActionsTests.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using RulesEngine.Enums; +using RulesEngine.Models; +using Xunit; + +namespace RulesEngine.UnitTest{ + + [ExcludeFromCodeCoverage] + public class RulesEngineWithActionsTests{ + + [Fact] + public async Task WhenExpressionIsSuccess_OutputExpressionAction_ReturnsExpressionEvaluation(){ + var engine = new RulesEngine(GetWorkflowWithActions()); + var result = await engine.ExecuteActionWorkflowAsync("ActionWorkflow", "ExpressionOutputRuleTest", new RuleParameter[0]); + Assert.NotNull(result); + Assert.Equal(2*2,result.Output); + } + + [Fact] + public async Task WhenExpressionIsSuccess_EvaluateRuleAction_ReturnsExpressionEvaluation() + { + var engine = new RulesEngine(GetWorkflowWithActions()); + var result = await engine.ExecuteActionWorkflowAsync("ActionWorkflow", "EvaluateRuleTest", new RuleParameter[0]); + Assert.NotNull(result); + Assert.Equal(2 * 2, result.Output); + Assert.Contains(result.Results, c => c.Rule.RuleName == "ExpressionOutputRuleTest"); + } + + [Fact] + public async Task ExecuteActionWorkflowAsync_CalledWithIncorrectWorkflowOrRuleName_ThrowsArgumentException() + { + var engine = new RulesEngine(GetWorkflowWithActions()); + await Assert.ThrowsAsync(async () => await engine.ExecuteActionWorkflowAsync("WrongWorkflow", "ExpressionOutputRuleTest", new RuleParameter[0])); + await Assert.ThrowsAsync(async () => await engine.ExecuteActionWorkflowAsync("ActionWorkflow", "WrongRule", new RuleParameter[0])); + } + + + [Fact] + public async Task ExecuteActionWorkflowAsync_CalledWithNoActionsInWorkflow_ExecutesSuccessfully() + { + + var engine = new RulesEngine(GetWorkflowRulesWithoutActions()); + var result = await engine.ExecuteActionWorkflowAsync("NoActionWorkflow", "NoActionTest", new RuleParameter[0]); + Assert.NotNull(result); + Assert.Null(result.Output); + } + + + private WorkflowRules[] GetWorkflowRulesWithoutActions(){ + var workflow1 = new WorkflowRules{ + WorkflowName = "NoActionWorkflow", + Rules = new List{ + new Rule{ + RuleName = "NoActionTest", + RuleExpressionType = RuleExpressionType.LambdaExpression, + Expression = "1 == 1", + } + + } + }; + return new []{workflow1}; + } + + private WorkflowRules[] GetWorkflowWithActions(){ + + var workflow1 = new WorkflowRules{ + WorkflowName = "ActionWorkflow", + Rules = new List{ + new Rule{ + RuleName = "ExpressionOutputRuleTest", + RuleExpressionType = RuleExpressionType.LambdaExpression, + Expression = "1 == 1", + Actions = new Dictionary{ + { ActionTriggerType.onSuccess, new ActionInfo{ + Name = "OutputExpression", + Context = new Dictionary{ + {"expression", "2*2"} + } + }} + } + }, + new Rule{ + RuleName = "EvaluateRuleTest", + RuleExpressionType = RuleExpressionType.LambdaExpression, + Expression = "1 == 1", + Actions = new Dictionary{ + { ActionTriggerType.onSuccess, new ActionInfo{ + Name = "EvaluateRule", + Context = new Dictionary{ + {"workflowName", "ActionWorkflow"}, + {"ruleName","ExpressionOutputRuleTest"} + } + }} + } + } + + } + }; + return new []{workflow1}; + } + } +} \ No newline at end of file diff --git a/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs index 7d8653c..652ac52 100644 --- a/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs +++ b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs @@ -15,6 +15,7 @@ using Xunit; using Newtonsoft.Json.Converters; using RulesEngine.HelperFunctions; using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; namespace RulesEngine.UnitTest { @@ -32,7 +33,7 @@ namespace RulesEngine.UnitTest [Theory] [InlineData("rules2.json")] - public void RulesEngine_InjectedRules_ReturnsListOfRuleResultTree(string ruleFileName) + public async Task RulesEngine_InjectedRules_ReturnsListOfRuleResultTree(string ruleFileName) { var re = GetRulesEngine(ruleFileName); @@ -40,14 +41,14 @@ namespace RulesEngine.UnitTest dynamic input2 = GetInput2(); dynamic input3 = GetInput3(); - var result = re.ExecuteRule("inputWorkflowReference", input1, input2, input3); + var result = await re.ExecuteAllRulesAsync("inputWorkflowReference",input1, input2, input3); Assert.NotNull(result); Assert.IsType>(result); } [Theory] [InlineData("rules2.json")] - public void ExecuteRule_ReturnsListOfRuleResultTree(string ruleFileName) + public async Task ExecuteRule_ReturnsListOfRuleResultTree(string ruleFileName) { var re = GetRulesEngine(ruleFileName); @@ -55,15 +56,15 @@ namespace RulesEngine.UnitTest dynamic input2 = GetInput2(); dynamic input3 = GetInput3(); - List result = re.ExecuteRule("inputWorkflow", input1, input2, input3); + List result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); Assert.NotNull(result); Assert.IsType>(result); - Assert.Contains(result, c => c.IsSuccess); + Assert.Contains(result,c => c.IsSuccess); } [Theory] [InlineData("rules2.json")] - public void ExecuteRule_SingleObject_ReturnsListOfRuleResultTree(string ruleFileName) + public async Task ExecuteRule_CalledMultipleTimes_ReturnsSameResult(string ruleFileName) { var re = GetRulesEngine(ruleFileName); @@ -71,15 +72,42 @@ namespace RulesEngine.UnitTest dynamic input2 = GetInput2(); dynamic input3 = GetInput3(); - List result = re.ExecuteRule("inputWorkflow", input1); + 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); + Assert.DoesNotContain(result,c => c.IsSuccess); } [Theory] [InlineData("rules3.json")] - public void ExecuteRule_ExceptionScenario_RulesInvalid(string ruleFileName) + public async Task ExecuteRule_ExceptionScenario_RulesInvalid(string ruleFileName) { var re = GetRulesEngine(ruleFileName); @@ -87,14 +115,14 @@ namespace RulesEngine.UnitTest dynamic input2 = GetInput2(); dynamic input3 = GetInput3(); - List result = re.ExecuteRule("inputWorkflow", input1, input2, input3); + 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")] - public void ExecuteRule_ReturnsListOfRuleResultTree_ResultMessage(string ruleFileName) + public async Task ExecuteRule_ReturnsListOfRuleResultTree_ResultMessage(string ruleFileName) { var re = GetRulesEngine(ruleFileName); @@ -102,7 +130,7 @@ namespace RulesEngine.UnitTest dynamic input2 = GetInput2(); dynamic input3 = GetInput3(); - List result = re.ExecuteRule("inputWorkflow", input1, input2, input3); + List result = await re.ExecuteAllRulesAsync("inputWorkflow",input1, input2, input3); Assert.NotNull(result); Assert.NotNull(result.First().GetMessages()); Assert.NotNull(result.First().GetMessages().WarningMessages); @@ -124,33 +152,37 @@ namespace RulesEngine.UnitTest }); } + [Theory] [InlineData("rules1.json")] - public void ExecuteRule_InvalidWorkFlow_ThrowsException(string ruleFileName) + public async Task ExecuteRule_InvalidWorkFlow_ThrowsException(string ruleFileName) { var re = GetRulesEngine(ruleFileName); dynamic input = GetInput1(); - Assert.Throws(() => { re.ExecuteRule("inputWorkflow1", input); }); + await Assert.ThrowsAsync(async() => { await re.ExecuteAllRulesAsync("inputWorkflow1", input); }); } [Theory] [InlineData("rules1.json")] - public void RemoveWorkflow_RemovesWorkflow(string ruleFileName) + public async Task RemoveWorkflow_RemovesWorkflow(string ruleFileName) { var re = GetRulesEngine(ruleFileName); - re.RemoveWorkflow("inputWorkflow"); - dynamic input1 = GetInput1(); dynamic input2 = GetInput2(); dynamic input3 = GetInput3(); - Assert.Throws(() => re.ExecuteRule("inputWorkflow", input1, input2, input3)); + 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 void ClearWorkflow_RemovesAllWorkflow(string ruleFileName) + public async Task ClearWorkflow_RemovesAllWorkflow(string ruleFileName) { var re = GetRulesEngine(ruleFileName); re.ClearWorkflows(); @@ -159,14 +191,15 @@ namespace RulesEngine.UnitTest dynamic input2 = GetInput2(); dynamic input3 = GetInput3(); - Assert.Throws(() => re.ExecuteRule("inputWorkflow", input1, input2, input3)); - Assert.Throws(() => re.ExecuteRule("inputWorkflowReference", input1, input2, input3)); + 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 void ExecuteRule_InputWithVariableProps_ReturnsResult(string ruleFileName) + public async Task ExecuteRule_InputWithVariableProps_ReturnsResult(string ruleFileName) { var re = GetRulesEngine(ruleFileName); @@ -174,22 +207,23 @@ namespace RulesEngine.UnitTest dynamic input2 = GetInput2(); dynamic input3 = GetInput3(); - List result = re.ExecuteRule("inputWorkflow", input1, input2, input3); + List result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); Assert.NotNull(result); Assert.IsType>(result); - Assert.Contains(result, c => c.IsSuccess); + Assert.Contains(result,c => c.IsSuccess); input3.hello = "world"; - result = re.ExecuteRule("inputWorkflow", input1, input2, input3); + result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3); Assert.NotNull(result); Assert.IsType>(result); - Assert.Contains(result, c => c.IsSuccess); + Assert.Contains(result,c => c.IsSuccess); } + [Theory] [InlineData("rules4.json")] - public void RulesEngine_Execute_Rule_For_Nested_Rule_Params_Returns_Success(string ruleFileName) + public async Task RulesEngine_Execute_Rule_For_Nested_Rule_Params_Returns_Success(string ruleFileName) { dynamic[] inputs = GetInputs4(); @@ -208,99 +242,99 @@ namespace RulesEngine.UnitTest var fileData = File.ReadAllText(files[0]); var bre = new RulesEngine(JsonConvert.DeserializeObject(fileData), null); - var result = bre.ExecuteRule("inputWorkflow", ruleParams?.ToArray()); ; + 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 void ExecuteRule_ReturnsProperErrorOnMissingRuleParameter(string ruleFileName) + 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 input1 = new RuleParameter("customName",GetInput1()); + var input2 = new RuleParameter("input2",GetInput2()); + var input3 = new RuleParameter("input3",GetInput3()); - List result = re.ExecuteRule("inputWorkflow", input1, input2, input3); + List 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 void ExecuteRule_WithInjectedUtils_ReturnsListOfRuleResultTree(string ruleFileName, string propValue, bool expectedResult) + [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 = propValue; + + if(propValue == null) + input1.Property1 = null; - if (propValue == null) - input1.Property1 = null; - var utils = new TestInstanceUtils(); - List result = re.ExecuteRule("inputWorkflow", new RuleParameter("input1", input1), new RuleParameter("utils", utils)); + List 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] + Assert.All(result,c => Assert.Equal(expectedResult,c.IsSuccess)); + } + + [Theory] [InlineData("rules6.json")] - public void ExecuteRule_RuleWithMethodExpression_ReturnsSucess(string ruleFileName) + 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; - + + dynamic input1 = new ExpandoObject(); + input1.Property1 = "hello"; + input1.Boolean = false; + input1.Method = func; + var utils = new TestInstanceUtils(); - List result = re.ExecuteRule("inputWorkflow", new RuleParameter("input1", input1)); + List result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); Assert.NotNull(result); Assert.IsType>(result); Assert.All(result, c => Assert.True(c.IsSuccess)); - } - - [Theory] + } + + [Theory] [InlineData("rules7.json")] - public void ExecuteRule_RuleWithUnaryExpression_ReturnsSucess(string ruleFileName) + public async Task ExecuteRule_RuleWithUnaryExpression_ReturnsSucess(string ruleFileName) { var re = GetRulesEngine(ruleFileName); - dynamic input1 = new ExpandoObject(); - input1.Boolean = false; - + dynamic input1 = new ExpandoObject(); + input1.Boolean = false; + var utils = new TestInstanceUtils(); - List result = re.ExecuteRule("inputWorkflow", new RuleParameter("input1", input1)); + List result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); Assert.NotNull(result); Assert.IsType>(result); Assert.All(result, c => Assert.True(c.IsSuccess)); - } - - [Theory] + } + + [Theory] [InlineData("rules8.json")] - public void ExecuteRule_RuleWithMemberAccessExpression_ReturnsSucess(string ruleFileName) + public async Task ExecuteRule_RuleWithMemberAccessExpression_ReturnsSucess(string ruleFileName) { var re = GetRulesEngine(ruleFileName); - dynamic input1 = new ExpandoObject(); - input1.Boolean = false; - + dynamic input1 = new ExpandoObject(); + input1.Boolean = false; + var utils = new TestInstanceUtils(); - List result = re.ExecuteRule("inputWorkflow", new RuleParameter("input1", input1)); + List result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); Assert.NotNull(result); Assert.IsType>(result); Assert.All(result, c => Assert.False(c.IsSuccess)); @@ -308,40 +342,39 @@ namespace RulesEngine.UnitTest [Theory] [InlineData("rules9.json")] - public void ExecuteRule_MissingMethodInExpression_ReturnsException(string ruleFileName) - { + 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; - + dynamic input1 = new ExpandoObject(); + input1.Data = new { TestProperty = "" }; + input1.Boolean = false; + var utils = new TestInstanceUtils(); - Assert.Throws(()=> - { - List result = re.ExecuteRule("inputWorkflow", new RuleParameter("input1", input1)); + await Assert.ThrowsAsync(async()=> + { + List result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); }); } [Theory] [InlineData("rules9.json")] - public void ExecuteRule_MissingMethodInExpression_DefaultParameter(string ruleFileName) - { + public async Task ExecuteRule_MissingMethodInExpression_DefaultParameter(string ruleFileName) + { var re = GetRulesEngine(ruleFileName); - dynamic input1 = new ExpandoObject(); - input1.Data = new { TestProperty = "" }; - input1.Boolean = false; - + dynamic input1 = new ExpandoObject(); + input1.Data = new { TestProperty = "" }; + input1.Boolean = false; + var utils = new TestInstanceUtils(); - List result = re.ExecuteRule("inputWorkflow", new RuleParameter("input1", input1)); + List result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); Assert.NotNull(result); Assert.IsType>(result); Assert.All(result, c => Assert.False(c.IsSuccess)); } - private RulesEngine CreateRulesEngine(WorkflowRules workflow) { var json = JsonConvert.SerializeObject(workflow); @@ -364,6 +397,7 @@ namespace RulesEngine.UnitTest return new RulesEngine(new string[] { data, injectWorkflowStr }, mockLogger.Object, reSettings); } + private dynamic GetInput1() { var converter = new ExpandoObjectConverter(); @@ -399,13 +433,11 @@ namespace RulesEngine.UnitTest var laborCategoriesInput = "[{\"country\": \"india\", \"loyalityFactor\": 2, \"totalPurchasesToDate\": 20000}]"; var currentLaborCategoryInput = "{\"CurrentLaborCategoryProp\":\"TestVal2\"}"; - var converter = new ExpandoObjectConverter(); - dynamic input1 = JsonConvert.DeserializeObject>(laborCategoriesInput); - dynamic input2 = JsonConvert.DeserializeObject(currentLaborCategoryInput, converter); - dynamic input3 = JsonConvert.DeserializeObject(telemetryInfo, converter); - dynamic input4 = JsonConvert.DeserializeObject(basicInfo, converter); - dynamic input5 = JsonConvert.DeserializeObject(orderInfo, converter); + dynamic input2 = JsonConvert.DeserializeObject(currentLaborCategoryInput); + dynamic input3 = JsonConvert.DeserializeObject(telemetryInfo); + dynamic input4 = JsonConvert.DeserializeObject(basicInfo); + dynamic input5 = JsonConvert.DeserializeObject(orderInfo); var inputs = new dynamic[] { @@ -420,14 +452,14 @@ namespace RulesEngine.UnitTest } [ExcludeFromCodeCoverage] - private class TestInstanceUtils - { - public bool CheckExists(string str) - { - if (str != null && str.Length > 0) + private class TestInstanceUtils{ + public bool CheckExists(string str){ + if(str != null && str.Length > 0) return true; return false; } + } + } } \ No newline at end of file diff --git a/test/RulesEngine.UnitTest/CustomTypeProviderTests.cs b/test/RulesEngine.UnitTest/CustomTypeProviderTests.cs index 170b3c7..6e55a0f 100644 --- a/test/RulesEngine.UnitTest/CustomTypeProviderTests.cs +++ b/test/RulesEngine.UnitTest/CustomTypeProviderTests.cs @@ -5,21 +5,18 @@ using RulesEngine; using Moq; using System; using Xunit; +using System.Diagnostics.CodeAnalysis; namespace RulesEngine.UnitTest { [Trait("Category", "Unit")] + [ExcludeFromCodeCoverage] public class CustomTypeProviderTests : IDisposable { private MockRepository mockRepository; - - - public CustomTypeProviderTests() { this.mockRepository = new MockRepository(MockBehavior.Strict); - - } public void Dispose() diff --git a/test/RulesEngine.UnitTest/ExpressionUtilsTest.cs b/test/RulesEngine.UnitTest/ExpressionUtilsTest.cs index 3872e56..d8a9c25 100644 --- a/test/RulesEngine.UnitTest/ExpressionUtilsTest.cs +++ b/test/RulesEngine.UnitTest/ExpressionUtilsTest.cs @@ -2,11 +2,13 @@ // Licensed under the MIT License. using RulesEngine.HelperFunctions; +using System.Diagnostics.CodeAnalysis; using Xunit; namespace RulesEngine.UnitTest { [Trait("Category", "Unit")] + [ExcludeFromCodeCoverage] public class ExpressionUtilsTest { [Fact] diff --git a/test/RulesEngine.UnitTest/LambdaExpressionBuilderTest.cs b/test/RulesEngine.UnitTest/LambdaExpressionBuilderTest.cs index e73ae77..8466fe3 100644 --- a/test/RulesEngine.UnitTest/LambdaExpressionBuilderTest.cs +++ b/test/RulesEngine.UnitTest/LambdaExpressionBuilderTest.cs @@ -2,26 +2,32 @@ // Licensed under the MIT License. using RulesEngine; +using RulesEngine.ExpressionBuilders; using RulesEngine.Models; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using Xunit; namespace RulesEngine.UnitTest { [Trait("Category", "Unit")] + [ExcludeFromCodeCoverage] public class LambdaExpressionBuilderTest { [Fact] public void BuildExpressionForRuleTest() { - var objBuilderFactory = new RuleExpressionBuilderFactory(new ReSettings()); + var reSettings = new ReSettings(); + var objBuilderFactory = new RuleExpressionBuilderFactory(reSettings,new RuleExpressionParser(reSettings)); var builder = objBuilderFactory.RuleGetExpressionBuilder(RuleExpressionType.LambdaExpression); - var parameterExpressions = new List(); - parameterExpressions.Add(Expression.Parameter(typeof(string), "RequestType")); - parameterExpressions.Add(Expression.Parameter(typeof(string), "RequestStatus")); - parameterExpressions.Add(Expression.Parameter(typeof(string), "RegistrationStatus")); + var ruleParameters = new RuleParameter[] { + new RuleParameter("RequestType","Sales"), + new RuleParameter("RequestStatus", "Active"), + new RuleParameter("RegistrationStatus", "InProcess") + }; + Rule mainRule = new Rule(); mainRule.RuleName = "rule1"; @@ -34,13 +40,10 @@ namespace RulesEngine.UnitTest dummyRule.Expression = "RequestType == \"vod\""; mainRule.Rules.Add(dummyRule); + var func = builder.BuildDelegateForRule(dummyRule, ruleParameters); - ParameterExpression ruleInputExp = Expression.Parameter(typeof(RuleInput), nameof(RuleInput)); - - var expression = builder.BuildExpressionForRule(dummyRule, parameterExpressions, ruleInputExp); - - Assert.NotNull(expression); - Assert.Equal(typeof(RuleResultTree), expression.ReturnType); + Assert.NotNull(func); + Assert.Equal(typeof(RuleResultTree), func.Method.ReturnType); } } } diff --git a/test/RulesEngine.UnitTest/ListofRuleResultTreeExtensionTest.cs b/test/RulesEngine.UnitTest/ListofRuleResultTreeExtensionTest.cs index acf1f82..1774ccc 100644 --- a/test/RulesEngine.UnitTest/ListofRuleResultTreeExtensionTest.cs +++ b/test/RulesEngine.UnitTest/ListofRuleResultTreeExtensionTest.cs @@ -2,12 +2,14 @@ using RulesEngine.Models; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text; using Xunit; namespace RulesEngine.UnitTest { [Trait("Category", "Unit")] + [ExcludeFromCodeCoverage] public class ListofRuleResultTreeExtensionTest { [Fact] @@ -19,7 +21,7 @@ namespace RulesEngine.UnitTest { ChildResults = null, ExceptionMessage = string.Empty, - Input = new object(), + Inputs = new Dictionary(), IsSuccess = true, Rule = new Rule() { @@ -30,7 +32,7 @@ namespace RulesEngine.UnitTest { ChildResults = null, ExceptionMessage = string.Empty, - Input = new object(), + Inputs = new Dictionary(), IsSuccess = false, Rule = new Rule() { @@ -59,7 +61,7 @@ namespace RulesEngine.UnitTest { ChildResults = null, ExceptionMessage = string.Empty, - Input = new object(), + Inputs = new Dictionary(), IsSuccess = true, Rule = new Rule() { @@ -71,7 +73,7 @@ namespace RulesEngine.UnitTest { ChildResults = null, ExceptionMessage = string.Empty, - Input = new object(), + Inputs = new Dictionary(), IsSuccess = false, Rule = new Rule() { @@ -100,7 +102,7 @@ namespace RulesEngine.UnitTest { ChildResults = null, ExceptionMessage = string.Empty, - Input = new object(), + Inputs = new Dictionary(), IsSuccess = false, Rule = new Rule() { @@ -111,7 +113,7 @@ namespace RulesEngine.UnitTest { ChildResults = null, ExceptionMessage = string.Empty, - Input = new object(), + Inputs = new Dictionary(), IsSuccess = false, Rule = new Rule() { @@ -141,7 +143,7 @@ namespace RulesEngine.UnitTest { ChildResults = null, ExceptionMessage = string.Empty, - Input = new object(), + Inputs = new Dictionary(), IsSuccess = true, Rule = new Rule() { @@ -152,7 +154,7 @@ namespace RulesEngine.UnitTest { ChildResults = null, ExceptionMessage = string.Empty, - Input = new object(), + Inputs = new Dictionary(), IsSuccess = false, Rule = new Rule() { @@ -181,7 +183,7 @@ namespace RulesEngine.UnitTest { ChildResults = null, ExceptionMessage = string.Empty, - Input = new object(), + Inputs = new Dictionary(), IsSuccess = false, Rule = new Rule() { @@ -192,7 +194,7 @@ namespace RulesEngine.UnitTest { ChildResults = null, ExceptionMessage = string.Empty, - Input = new object(), + Inputs = new Dictionary(), IsSuccess = false, Rule = new Rule() { diff --git a/test/RulesEngine.UnitTest/PrivateSetterContractResolver.cs b/test/RulesEngine.UnitTest/PrivateSetterContractResolver.cs deleted file mode 100644 index 3782a28..0000000 --- a/test/RulesEngine.UnitTest/PrivateSetterContractResolver.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Text; - -namespace RulesEngine.UnitTest -{ - /// Class PrivateSetterContractResolver. - /// Implements the - public class PrivateSetterContractResolver : DefaultContractResolver - { - /// Creates a for the given MemberInfo. - /// The member to create a for. - /// The member's parent . - /// A created for the given MemberInfo. - protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) - { - var jsonProperty = base.CreateProperty(member, memberSerialization); - if (!jsonProperty.Writable) - { - if (member is PropertyInfo propertyInfo) - { - jsonProperty.Writable = propertyInfo.GetSetMethod(true) != null; - } - } - - return jsonProperty; - } - } -} diff --git a/test/RulesEngine.UnitTest/RuleCompilerTest.cs b/test/RulesEngine.UnitTest/RuleCompilerTest.cs index 0faf4ac..4dd5f22 100644 --- a/test/RulesEngine.UnitTest/RuleCompilerTest.cs +++ b/test/RulesEngine.UnitTest/RuleCompilerTest.cs @@ -2,28 +2,35 @@ // Licensed under the MIT License. using Microsoft.Extensions.Logging.Abstractions; +using RulesEngine.ExpressionBuilders; using RulesEngine.Models; using System; +using System.Diagnostics.CodeAnalysis; using Xunit; namespace RulesEngine.UnitTest { [Trait("Category","Unit")] + [ExcludeFromCodeCoverage] public class RuleCompilerTest { [Fact] public void RuleCompiler_NullCheck() { Assert.Throws(() => new RuleCompiler(null, null)); - Assert.Throws(() => new RuleCompiler(new RuleExpressionBuilderFactory(new ReSettings()), null)); + var reSettings = new ReSettings(); + var parser = new RuleExpressionParser(reSettings); + Assert.Throws(() => new RuleCompiler(new RuleExpressionBuilderFactory(reSettings,parser), null)); } [Fact] public void RuleCompiler_CompileRule_ThrowsException() { - var compiler = new RuleCompiler(new RuleExpressionBuilderFactory(new ReSettings()), new NullLogger()); - Assert.Throws(() => compiler.CompileRule(null, null)); - Assert.Throws(() => compiler.CompileRule(null, new RuleParameter[] { null})); + var reSettings = new ReSettings(); + var parser = new RuleExpressionParser(reSettings); + var compiler = new RuleCompiler(new RuleExpressionBuilderFactory(reSettings,parser), new NullLogger()); + Assert.Throws(() => compiler.CompileRule(null, null)); + Assert.Throws(() => compiler.CompileRule(null, new RuleParameter[] { null})); } diff --git a/test/RulesEngine.UnitTest/RuleExpressionBuilderFactoryTest.cs b/test/RulesEngine.UnitTest/RuleExpressionBuilderFactoryTest.cs index dde2e2c..00558f5 100644 --- a/test/RulesEngine.UnitTest/RuleExpressionBuilderFactoryTest.cs +++ b/test/RulesEngine.UnitTest/RuleExpressionBuilderFactoryTest.cs @@ -5,18 +5,22 @@ using RulesEngine; using RulesEngine.ExpressionBuilders; using RulesEngine.Models; using System; +using System.Diagnostics.CodeAnalysis; using Xunit; namespace RulesEngine.UnitTest { [Trait("Category", "Unit")] + [ExcludeFromCodeCoverage] public class RuleExpressionBuilderFactoryTest { [Theory] [InlineData(RuleExpressionType.LambdaExpression, typeof(LambdaExpressionBuilder))] public void RuleGetExpressionBuilderTest(RuleExpressionType expressionType, Type expectedExpressionBuilderType) { - var objBuilderFactory = new RuleExpressionBuilderFactory(new ReSettings()); + var reSettings = new ReSettings(); + var parser = new RuleExpressionParser(reSettings); + var objBuilderFactory = new RuleExpressionBuilderFactory(reSettings,parser); var builder = objBuilderFactory.RuleGetExpressionBuilder(expressionType); var builderType = builder.GetType(); diff --git a/test/RulesEngine.UnitTest/RuleTestClass.cs b/test/RulesEngine.UnitTest/RuleTestClass.cs index 77407f3..40c0ece 100644 --- a/test/RulesEngine.UnitTest/RuleTestClass.cs +++ b/test/RulesEngine.UnitTest/RuleTestClass.cs @@ -1,40 +1,19 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text; namespace RulesEngine.UnitTest { - /// - /// Class RuleTestClass. - /// + [ExcludeFromCodeCoverage] public class RuleTestClass { - /// - /// Gets the country. - /// - /// - /// The country. - /// [JsonProperty("country")] - public string Country { get; private set; } + public string Country { get; set; } - /// - /// Gets the loyality factor. - /// - /// - /// The loyality factor. - /// [JsonProperty("loyalityFactor")] - public int LoyalityFactor { get; private set; } - - /// - /// Gets the total purchases to date. - /// - /// - /// The total purchases to date. - /// - [JsonProperty("totalPurchasesToDate")] - public int TotalPurchasesToDate { get; private set; } + public int LoyalityFactor { get; set; } + public int TotalPurchasesToDate { get; set; } } } diff --git a/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj b/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj index 3c83e7f..d5e1808 100644 --- a/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj +++ b/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj @@ -1,25 +1,21 @@  - - Exe netcoreapp3.1 - - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive + - - + - PreserveNewest @@ -33,21 +29,20 @@ PreserveNewest - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + - - + \ No newline at end of file diff --git a/test/RulesEngine.UnitTest/TestData/rules4.json b/test/RulesEngine.UnitTest/TestData/rules4.json index fe2c0ec..097d6d4 100644 --- a/test/RulesEngine.UnitTest/TestData/rules4.json +++ b/test/RulesEngine.UnitTest/TestData/rules4.json @@ -42,6 +42,16 @@ "SuccessEvent": "25", "ErrorMessage": "One or more adjust rules failed, country : $(input4.country), loyalityFactor : $(input4.loyalityFactor), totalPurchasesToDate : $(input4.totalPurchasesToDate), totalOrders : $(input5.totalOrders), noOfVisitsPerMonth : $(input30.noOfVisitsPerMonth)", "ErrorType": "Error", + "localParams": [ + { + "Name": "model1", + "Expression": "input1.FirstOrDefault(country.Equals(\"india\", StringComparison.OrdinalIgnoreCase))" + }, + { + "Name": "model2", + "Expression": "model1.country == \"india\"" + } + ], "RuleExpressionType": "LambdaExpression", "Expression": "input4.country == \"india\" AND input4.loyalityFactor >= 2 AND input4.totalPurchasesToDate <= 10 AND input5.totalOrders > 2 AND input3.noOfVisitsPerMonth > 5" }, diff --git a/test/RulesEngine.UnitTest/TestData/rules5.json b/test/RulesEngine.UnitTest/TestData/rules5.json index 2faf81b..c58afd1 100644 --- a/test/RulesEngine.UnitTest/TestData/rules5.json +++ b/test/RulesEngine.UnitTest/TestData/rules5.json @@ -10,4 +10,4 @@ "Expression": "utils.CheckExists(String(input1.Property1)) == true" } ] -} \ No newline at end of file +} \ No newline at end of file diff --git a/test/RulesEngine.UnitTest/TestData/rules6.json b/test/RulesEngine.UnitTest/TestData/rules6.json index 6d047c3..6dfb8a9 100644 --- a/test/RulesEngine.UnitTest/TestData/rules6.json +++ b/test/RulesEngine.UnitTest/TestData/rules6.json @@ -1,29 +1,29 @@ { - "WorkflowName": "inputWorkflow", - "Rules": [ - { - "RuleName": "GiveDiscount10", - "SuccessEvent": "10", - "ErrorMessage": "One or more adjust rules failed.", - "ErrorType": "Error", - "RuleExpressionType": "LambdaExpression", - "Expression": "input1.Property1.Contains(\"hell\")" - }, - { - "RuleName": "GiveDiscount20", - "SuccessEvent": "20", - "ErrorMessage": "One or more adjust rules failed.", - "ErrorType": "Error", - "RuleExpressionType": "LambdaExpression", - "Expression": "input1.Property1.Contains(\"hell\") && !input1.Boolean" - }, - { - "RuleName": "GiveDiscount30", - "SuccessEvent": "30", - "ErrorMessage": "One or more adjust rules failed.", - "ErrorType": "Error", - "RuleExpressionType": "LambdaExpression", - "Expression": "input1.Method.Invoke()" - } - ] + "WorkflowName": "inputWorkflow", + "Rules": [ + { + "RuleName": "GiveDiscount10", + "SuccessEvent": "10", + "ErrorMessage": "One or more adjust rules failed.", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression", + "Expression": "input1.Property1.Contains(\"hell\")" + }, + { + "RuleName": "GiveDiscount20", + "SuccessEvent": "20", + "ErrorMessage": "One or more adjust rules failed.", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression", + "Expression": "input1.Property1.Contains(\"hell\") && !input1.Boolean" + }, + { + "RuleName": "GiveDiscount30", + "SuccessEvent": "30", + "ErrorMessage": "One or more adjust rules failed.", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression", + "Expression": "input1.Method.Invoke()" + } + ] } \ No newline at end of file diff --git a/test/RulesEngine.UnitTest/TestData/rules7.json b/test/RulesEngine.UnitTest/TestData/rules7.json index 135bcd0..ae2ff0d 100644 --- a/test/RulesEngine.UnitTest/TestData/rules7.json +++ b/test/RulesEngine.UnitTest/TestData/rules7.json @@ -1,21 +1,21 @@ { - "WorkflowName": "inputWorkflow", - "Rules": [ - { - "RuleName": "GiveDiscount10", - "SuccessEvent": "10", - "ErrorMessage": "One or more adjust rules failed.", - "ErrorType": "Error", - "RuleExpressionType": "LambdaExpression", - "Expression": "!input1.Boolean" - }, - { - "RuleName": "GiveDiscount20", - "SuccessEvent": "20", - "ErrorMessage": "One or more adjust rules failed.", - "ErrorType": "Error", - "RuleExpressionType": "LambdaExpression", - "Expression": "!input1.Boolean && true" - } - ] + "WorkflowName": "inputWorkflow", + "Rules": [ + { + "RuleName": "GiveDiscount10", + "SuccessEvent": "10", + "ErrorMessage": "One or more adjust rules failed.", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression", + "Expression": "!input1.Boolean" + }, + { + "RuleName": "GiveDiscount20", + "SuccessEvent": "20", + "ErrorMessage": "One or more adjust rules failed.", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression", + "Expression": "!input1.Boolean && true" + } + ] } \ No newline at end of file diff --git a/test/RulesEngine.UnitTest/TestData/rules8.json b/test/RulesEngine.UnitTest/TestData/rules8.json index 625a9e1..d15820d 100644 --- a/test/RulesEngine.UnitTest/TestData/rules8.json +++ b/test/RulesEngine.UnitTest/TestData/rules8.json @@ -1,21 +1,21 @@ { - "WorkflowName": "inputWorkflow", - "Rules": [ - { - "RuleName": "GiveDiscount10", - "SuccessEvent": "10", - "ErrorMessage": "One or more adjust rules failed.", - "ErrorType": "Error", - "RuleExpressionType": "LambdaExpression", - "Expression": "input1.Boolean" - }, - { - "RuleName": "GiveDiscount20", - "SuccessEvent": "20", - "ErrorMessage": "One or more adjust rules failed.", - "ErrorType": "Error", - "RuleExpressionType": "LambdaExpression", - "Expression": "input1.Boolean && true || (input1.Boolean)" - } - ] + "WorkflowName": "inputWorkflow", + "Rules": [ + { + "RuleName": "GiveDiscount10", + "SuccessEvent": "10", + "ErrorMessage": "One or more adjust rules failed.", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression", + "Expression": "input1.Boolean" + }, + { + "RuleName": "GiveDiscount20", + "SuccessEvent": "20", + "ErrorMessage": "One or more adjust rules failed.", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression", + "Expression": "input1.Boolean && true || (input1.Boolean)" + } + ] } \ No newline at end of file diff --git a/test/RulesEngine.UnitTest/TestData/rules9.json b/test/RulesEngine.UnitTest/TestData/rules9.json index 4042953..dc54012 100644 --- a/test/RulesEngine.UnitTest/TestData/rules9.json +++ b/test/RulesEngine.UnitTest/TestData/rules9.json @@ -1,21 +1,21 @@ { - "WorkflowName": "inputWorkflow", - "Rules": [ - { - "RuleName": "GiveDiscount10", - "SuccessEvent": "10", - "ErrorMessage": "One or more adjust rules failed.", - "ErrorType": "Error", - "RuleExpressionType": "LambdaExpression", - "Expression": "input1.Boolean" - }, - { - "RuleName": "GiveDiscount20", - "SuccessEvent": "20", - "ErrorMessage": "One or more adjust rules failed.", - "ErrorType": "Error", - "RuleExpressionType": "LambdaExpression", - "Expression": "input1.Boolean && input1.Data.NotExistingMethod()" - } - ] -} \ No newline at end of file + "WorkflowName": "inputWorkflow", + "Rules": [ + { + "RuleName": "GiveDiscount10", + "SuccessEvent": "10", + "ErrorMessage": "One or more adjust rules failed.", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression", + "Expression": "input1.Boolean" + }, + { + "RuleName": "GiveDiscount20", + "SuccessEvent": "20", + "ErrorMessage": "One or more adjust rules failed.", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression", + "Expression": "input1.Boolean && input1.Data.NotExistingMethod()" + } + ] + } \ No newline at end of file diff --git a/test/RulesEngine.UnitTest/UtilsTests.cs b/test/RulesEngine.UnitTest/UtilsTests.cs index b0d3625..0056ec9 100644 --- a/test/RulesEngine.UnitTest/UtilsTests.cs +++ b/test/RulesEngine.UnitTest/UtilsTests.cs @@ -4,12 +4,14 @@ using RulesEngine.HelperFunctions; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Dynamic; using System.Text; using Xunit; namespace RulesEngine.UnitTest { + [ExcludeFromCodeCoverage] public class TestClass { public string test { get; set; } @@ -17,6 +19,7 @@ namespace RulesEngine.UnitTest } [Trait("Category","Unit")] + [ExcludeFromCodeCoverage] public class UtilsTests { @@ -44,7 +47,6 @@ namespace RulesEngine.UnitTest object typedobj2 = Utils.GetTypedObject(obj2); Assert.IsNotType(typedobj); Assert.NotNull(typedobj.GetType().GetProperty("test")); - Console.WriteLine($"{typedobj.GetType()} & {typedobj2.GetType()}"); Assert.Equal(typedobj.GetType(),typedobj2.GetType()); }