Merge develop to master (#68)

* Users/abbasc52/add-actions (#56)

* Removed ruleinput

* Simplified compiled params code

* clean up and renaming

* Fixed caching and made test cases more specific

* updated settings

* updated nuget and fix warnings

* Added test case for invalid input name

* changed input from object to dictionary

* Added action related fields and added expression output action

* Added evaluateRuleAction and added basic tests for actions

* made action names and context case insensitive

* Added exception handling

* fixed exception message for parsing error

* improved constructor for action factory

* Added build trigger for pr to develop

* Added more testcases for actions

* Shared same instance of parser for RulesEngine and OutputExpression action

* fixed review comments

* Added null check for actions not mentioned in json

* pull fixes from master (#61)

* Renamed ExecuteRule to ExecuteAllRulesAsync and added action support (#63)

* Added support for actions
* Renamed ExecuteRule to ExecuteAllRulesAsync

* added github nuget publish
pull/66/head
Abbas Cyclewala 2020-11-02 09:25:43 +05:30 committed by GitHub
parent 0a394a7f79
commit b68861dfd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
82 changed files with 1850 additions and 1572 deletions

12
.config/dotnet-tools.json Normal file
View File

@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-reportgenerator-globaltool": {
"version": "4.7.1",
"commands": [
"reportgenerator"
]
}
}
}

View File

@ -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

1
.gitignore vendored
View File

@ -330,3 +330,4 @@ ASALocalRun/
.mfractor/
/src/RulesEngine/RulesEngine.sln.licenseheader
/assets/RulesEnginePackageFile.xml
coveragereport/

27
.vscode/launch.json vendored Normal file
View File

@ -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}"
}
]
}

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"dotnetCoreExplorer.searchpatterns": "test/**/bin/Debug/netcoreapp*/*.{dll,exe,json}",
"coverage-gutters.coverageBaseDir": "coveragereport",
"coverage-gutters.coverageReportFileName": "coveragereport/**/index.html"
}

42
.vscode/tasks.json vendored Normal file
View File

@ -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"
}
]
}

View File

@ -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.

View File

@ -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<RuleResultTree> response = rulesEngine.ExecuteRule(workflowName, input);
List<RuleResultTree> 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.

View File

@ -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

View File

@ -42,7 +42,7 @@ namespace DemoApp
string discountOffered = "No discount offered.";
List<RuleResultTree> resultList = bre.ExecuteRule("Discount", inputs);
List<RuleResultTree> resultList = bre.ExecuteAllRulesAsync("Discount", inputs).Result;
resultList.OnSuccess((eventName) =>
{

View File

@ -8,7 +8,7 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<ProjectReference Include="../../src/RulesEngine/RulesEngine/RulesEngine.csproj" />
<ProjectReference Include="../../src/RulesEngine/RulesEngine.csproj" />
</ItemGroup>
<ItemGroup>

View File

@ -50,7 +50,7 @@ namespace DemoApp
var bre = new RulesEngine.RulesEngine(workflowRules.ToArray(),null);
foreach(var workflow in workflowRules)
{
List<RuleResultTree> resultList = bre.ExecuteRule(workflow.WorkflowName, nestedInput);
List<RuleResultTree> resultList = bre.ExecuteAllRulesAsync(workflow.WorkflowName, nestedInput).Result;
resultList.OnSuccess((eventName) =>
{

View File

@ -1,6 +1,7 @@
{
"sdk": {
"version": "3.1.101",
"rollForward": "latestFeature"
"version": "3.1",
"rollForward": "latestFeature",
"allowPrerelease": false
}
}

View File

@ -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<ActionRuleResult> 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<RuleResultTree>()
{
context.GetParentRuleResult()
};
}
}
return result;
}
public abstract ValueTask<object> Run(ActionContext context, RuleParameter[] ruleParameters);
}
}

View File

@ -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<string, string> _context;
private readonly RuleResultTree _parentResult;
public ActionContext(IDictionary<string, object> context, RuleResultTree parentResult)
{
_context = new Dictionary<string, string>(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<T>(string name)
{
try
{
if (typeof(T) == typeof(string))
{
return (T)Convert.ChangeType(_context[name], typeof(T));
}
return JsonConvert.DeserializeObject<T>(_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");
}
}
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
namespace RulesEngine.Actions
{
internal class ActionFactory
{
private readonly IDictionary<string, Func<ActionBase>> _actionRegistry;
internal ActionFactory()
{
_actionRegistry = new Dictionary<string, Func<ActionBase>>(StringComparer.OrdinalIgnoreCase);
}
internal ActionFactory(IDictionary<string,Func<ActionBase>> 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");
}
}
}

View File

@ -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<ActionRuleResult> ExecuteAndReturnResultAsync(ActionContext context, RuleParameter[] ruleParameters, bool includeRuleResults=false){
var innerResult = await base.ExecuteAndReturnResultAsync(context,ruleParameters,includeRuleResults);
var output = innerResult.Output as ActionRuleResult;
List<RuleResultTree> resultList = null;
if(includeRuleResults){
resultList = new List<RuleResultTree>(output.Results);
resultList.AddRange(innerResult.Results);
}
return new ActionRuleResult {
Output = output.Output,
Exception = innerResult.Exception,
Results = resultList
};
}
public override async ValueTask<object> Run(ActionContext context, RuleParameter[] ruleParameters)
{
var workflowName = context.GetContext<string>("workflowName");
var ruleName = context.GetContext<string>("ruleName");
var ruleResult = await _ruleEngine.ExecuteActionWorkflowAsync(workflowName,ruleName,ruleParameters);
return ruleResult;
}
}
}

View File

@ -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<object> Run(ActionContext context, RuleParameter[] ruleParameters)
{
var expression = context.GetContext<string>("expression");
return new ValueTask<object>(_ruleExpressionParser.Evaluate(expression, ruleParameters));
}
}
}

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace RulesEngine.Enums
{
public enum ActionTriggerType
{
onSuccess,
onFailure
}
}

View File

@ -0,0 +1,44 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using RulesEngine.HelperFunctions;
using RulesEngine.Models;
using System;
namespace RulesEngine.ExpressionBuilders
{
/// <summary>
/// This class will build the list expression
/// </summary>
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<RuleResultTree> 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);
}
}
}
}

View File

@ -20,13 +20,6 @@ namespace RulesEngine.ExpressionBuilders
/// <param name="typeParamExpressions">The type parameter expressions.</param>
/// <param name="ruleInputExp">The rule input exp.</param>
/// <returns>Expression type</returns>
internal abstract Expression<Func<RuleInput, RuleResultTree>> BuildExpressionForRule(Rule rule, IEnumerable<ParameterExpression> typeParamExpressions, ParameterExpression ruleInputExp);
/// <summary>Builds the expression for rule parameter.</summary>
/// <param name="rule">The rule.</param>
/// <param name="typeParamExpressions">The type parameter expressions.</param>
/// <param name="ruleInputExp">The rule input exp.</param>
/// <returns>Expression.</returns>
internal abstract Expression BuildExpressionForRuleParam(LocalParam rule, IEnumerable<ParameterExpression> typeParamExpressions, ParameterExpression ruleInputExp);
internal abstract RuleFunc<RuleResultTree> BuildDelegateForRule(Rule rule, RuleParameter[] ruleParams);
}
}

View File

@ -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());
}
// <summary>
/// Gets the parameter expression.
/// </summary>
/// <param name="ruleParams">The types.</param>
/// <returns></returns>
/// <exception cref="ArgumentException">
/// types
/// or
/// type
/// </exception>
private IEnumerable<ParameterExpression> 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;
}
}
}

View File

@ -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
{
/// <summary>
/// Helpers
/// </summary>
internal static class Helpers
{
internal static RuleFunc<RuleResultTree> ToResultTree(Rule rule, IEnumerable<RuleResultTree> childRuleResults, Func<object[],bool> 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
};
}
/// <summary>
/// To the result tree error messages
/// </summary>
/// <param name="ruleResultTree">ruleResultTree</param>
/// <param name="ruleResultMessage">ruleResultMessage</param>
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);
}
}
}
}
/// <summary>
/// To get the child error message recersivly
/// </summary>
/// <param name="childResultTree">childResultTree</param>
/// <param name="ruleResultMessage">ruleResultMessage</param>
private static void GetChildRuleMessages(IEnumerable<RuleResultTree> childResultTree, ref RuleResultMessage ruleResultMessage)
{
foreach (var item in childResultTree)
{
ToResultTreeMessages(item, ref ruleResultMessage);
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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
/// <param name="workflowName">The name of the workflow with rules to execute against the inputs</param>
/// <param name="inputs">A variable number of inputs</param>
/// <returns>List of rule results</returns>
List<RuleResultTree> ExecuteRule(string workflowName, params object[] inputs);
ValueTask<List<RuleResultTree>> ExecuteAllRulesAsync(string workflowName, params object[] inputs);
/// <summary>
/// This will execute all the rules of the specified workflow
@ -22,6 +23,10 @@ namespace RulesEngine.Interfaces
/// <param name="workflowName">The name of the workflow with rules to execute against the inputs</param>
/// <param name="ruleParams">A variable number of rule parameters</param>
/// <returns>List of rule results</returns>
List<RuleResultTree> ExecuteRule(string workflowName, params RuleParameter[] ruleParams);
ValueTask<List<RuleResultTree>> ExecuteAllRulesAsync(string workflowName, params RuleParameter[] ruleParams);
ValueTask<ActionRuleResult> ExecuteActionWorkflowAsync(string workflowName, string ruleName, RuleParameter[] ruleParameters);
void AddWorkflow(params WorkflowRules[] workflowRules);
void ClearWorkflows();
void RemoveWorkflow(params string[] workflowNames);
}
}

View File

@ -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<string, object> Context { get; set; }
}
}

View File

@ -0,0 +1,6 @@
using System;
public class ActionResult{
public object Output {get; set;}
public Exception Exception { get; set; }
}

View File

@ -0,0 +1,7 @@
using System.Collections.Generic;
using RulesEngine.Models;
public class ActionRuleResult : ActionResult{
public List<RuleResultTree> Results {get; set;}
}

View File

@ -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
/// <summary>
/// CompiledParam class.
/// </summary>
[ExcludeFromCodeCoverage]
internal class CompiledParam
{
/// <summary>
@ -32,5 +34,10 @@ namespace RulesEngine.Models
/// The parameters.
/// </value>
internal IEnumerable<RuleParameter> Parameters { get; set; }
internal RuleParameter AsRuleParameter()
{
return new RuleParameter(Name,Value.Method.ReturnType);
}
}
}

View File

@ -1,9 +1,11 @@
using Newtonsoft.Json;
using System.Diagnostics.CodeAnalysis;
namespace RulesEngine.Models
{
/// <summary>Class Param.
/// Implements the <see cref="RulesEngine.Models.Rule" /></summary>
/// <summary>Class LocalParam.
/// </summary>
[ExcludeFromCodeCoverage]
public class LocalParam
{

View File

@ -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<string, Func<ActionBase>> CustomActions { get; set; }
public bool EnableExceptionAsErrorMessage { get; set; } = true;
}
}

View File

@ -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
{
/// <summary>
/// Rule class
/// </summary>
[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<string> WorkflowRulesToInject { get; set; }
public List<Rule> Rules { get; set; }
[JsonProperty]
public IEnumerable<LocalParam> LocalParams { get; private set; }
public string Expression { get; set; }
public Dictionary<ActionTriggerType, ActionInfo> Actions { get; set; }
public string SuccessEvent { get; set; }
}
}

View File

@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
namespace RulesEngine.Models
{
public delegate T RuleFunc<T>(params RuleParameter[] ruleParameters);
}

View File

@ -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; }

View File

@ -40,7 +40,9 @@ namespace RulesEngine.Models
/// <summary>
/// Gets or sets the input object
/// </summary>
public object Input { get; set; }
public Dictionary<string,object> Inputs { get; set; }
public ActionResult ActionResult {get; set;}
/// <summary>
/// Gets the exception message in case an error is thrown during rules calculation.

View File

@ -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
{
/// <summary>
/// Rule param compilers
/// </summary>
internal class ParamCompiler
{
private readonly ReSettings _reSettings;
private readonly RuleExpressionParser _ruleExpressionParser;
internal ParamCompiler(ReSettings reSettings, RuleExpressionParser ruleExpressionParser)
{
_reSettings = reSettings;
_ruleExpressionParser = ruleExpressionParser;
}
/// <summary>
/// Compiles the and evaluate parameter expression.
/// </summary>
/// <param name="rule">The rule.</param>
/// <param name="ruleParams">The rule parameters.</param>
/// <returns>
/// IEnumerable&lt;RuleParameter&gt;.
/// </returns>
public IEnumerable<CompiledParam> CompileParamsExpression(Rule rule, IEnumerable<RuleParameter> ruleParams)
{
if(rule.LocalParams == null) return null;
var compiledParameters = new List<CompiledParam>();
var evaluatedParameters = new List<RuleParameter>();
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;
}
/// <summary>Evaluates the compiled parameter.</summary>
/// <param name="paramName">Name of the parameter.</param>
/// <param name="compiledParam">The compiled parameter.</param>
/// <param name="ruleParams">The rule parameters.</param>
/// <returns>RuleParameter.</returns>
public RuleParameter EvaluateCompiledParam(string paramName, Delegate compiledParam, IEnumerable<RuleParameter> inputs)
{
var result = compiledParam.DynamicInvoke(inputs.Select(c => c.Value).ToArray());
return new RuleParameter(paramName, result);
}
/// <summary>
/// Gets the expression for rule.
/// </summary>
/// <param name="param">The rule.</param>
/// <param name="typeParameterExpressions">The type parameter expressions.</param>
/// <param name="ruleInputExp">The rule input exp.</param>
/// <returns></returns>
private Delegate GetDelegateForRuleParam(LocalParam param, RuleParameter[] ruleParameters)
{
return _ruleExpressionParser.Compile(param.Expression, ruleParameters);
}
}
}

View File

@ -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
{
/// <summary>
/// Rule compilers
/// </summary>
internal class RuleCompiler
{
/// <summary>
/// The nested operators
/// </summary>
private readonly ExpressionType[] nestedOperators = new ExpressionType[] { ExpressionType.And, ExpressionType.AndAlso, ExpressionType.Or, ExpressionType.OrElse };
/// <summary>
/// The expression builder factory
/// </summary>
private readonly RuleExpressionBuilderFactory _expressionBuilderFactory;
/// <summary>
/// The logger
/// </summary>
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="RuleCompiler"/> class.
/// </summary>
/// <param name="expressionBuilderFactory">The expression builder factory.</param>
/// <exception cref="ArgumentNullException">expressionBuilderFactory</exception>
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;
}
/// <summary>
/// Compiles the rule
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="rule"></param>
/// <param name="input"></param>
/// <param name="ruleParam"></param>
/// <returns>Compiled func delegate</returns>
internal RuleFunc<RuleResultTree> CompileRule(Rule rule,params RuleParameter[] ruleParams)
{
try
{
if(rule == null)
{
throw new ArgumentNullException(nameof(rule));
}
RuleFunc<RuleResultTree> ruleExpression = GetDelegateForRule(rule,ruleParams);
return ruleExpression;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
throw;
}
}
/// <summary>
/// Gets the expression for rule.
/// </summary>
/// <param name="rule">The rule.</param>
/// <param name="typeParameterExpressions">The type parameter expressions.</param>
/// <param name="ruleInputExp">The rule input exp.</param>
/// <returns></returns>
private RuleFunc<RuleResultTree> 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);
}
}
/// <summary>
/// Builds the expression.
/// </summary>
/// <param name="rule">The rule.</param>
/// <param name="typeParameterExpressions">The type parameter expressions.</param>
/// <param name="ruleInputExp">The rule input exp.</param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
private RuleFunc<RuleResultTree> 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;
}
/// <summary>
/// Builds the nested expression.
/// </summary>
/// <param name="parentRule">The parent rule.</param>
/// <param name="childRules">The child rules.</param>
/// <param name="operation">The operation.</param>
/// <param name="typeParameterExpressions">The type parameter expressions.</param>
/// <param name="ruleInputExp">The rule input exp.</param>
/// <returns>Expression of func delegate</returns>
/// <exception cref="InvalidCastException"></exception>
private RuleFunc<RuleResultTree> BuildNestedRuleFunc(Rule parentRule, ExpressionType operation, RuleParameter[] ruleParams)
{
var ruleFuncList = new List<RuleFunc<RuleResultTree>>();
foreach (var r in parentRule.Rules)
{
ruleFuncList.Add(GetDelegateForRule(r, ruleParams));
}
return (paramArray) =>
{
var resultList = ruleFuncList.Select(fn => fn(paramArray));
Func<object[],bool> isSuccess = (p) => ApplyOperation(resultList, operation);
RuleFunc<RuleResultTree> result = Helpers.ToResultTree(parentRule, resultList,isSuccess);
return result(paramArray);
};
}
private bool ApplyOperation(IEnumerable<RuleResultTree> 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;
}
}
}
}

View File

@ -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.");
}

View File

@ -13,7 +13,7 @@ namespace RulesEngine
internal class RulesCache
{
/// <summary>The compile rules</summary>
private ConcurrentDictionary<string, IEnumerable<CompiledRule>> _compileRules = new ConcurrentDictionary<string, IEnumerable<CompiledRule>>();
private ConcurrentDictionary<string, IDictionary<string,RuleFunc<RuleResultTree>>> _compileRules = new ConcurrentDictionary<string, IDictionary<string,RuleFunc<RuleResultTree>>>();
/// <summary>The workflow rules</summary>
private ConcurrentDictionary<string, WorkflowRules> _workflowRules = new ConcurrentDictionary<string, WorkflowRules>();
@ -47,7 +47,7 @@ namespace RulesEngine
/// <summary>Adds the or update compiled rule.</summary>
/// <param name="compiledRuleKey">The compiled rule key.</param>
/// <param name="compiledRule">The compiled rule.</param>
public void AddOrUpdateCompiledRule(string compiledRuleKey, IEnumerable<CompiledRule> compiledRule)
public void AddOrUpdateCompiledRule(string compiledRuleKey, IDictionary<string,RuleFunc<RuleResultTree>> compiledRule)
{
_compileRules.AddOrUpdate(compiledRuleKey, compiledRule, (k, v) => compiledRule);
}
@ -64,6 +64,8 @@ namespace RulesEngine
/// <returns>IEnumerable&lt;Rule&gt;.</returns>
public IEnumerable<Rule> 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;
}
}
/// <summary>Gets the compiled rules.</summary>
/// <param name="compiledRulesKey">The compiled rules key.</param>
/// <returns>CompiledRule.</returns>
public IEnumerable<CompiledRule> GetCompiledRules(string compiledRulesKey)
public IDictionary<string, RuleFunc<RuleResultTree>> 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<CompiledRule> val);
_compileRules.TryRemove(key, out IDictionary<string,RuleFunc<RuleResultTree>> val);
}
}
}

View File

@ -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
{
/// <summary>
///
/// </summary>
/// <seealso cref="RulesEngine.Interfaces.IRulesEngine" />
public class RulesEngine : IRulesEngine
{
#region Variables
private readonly ILogger _logger;
private readonly ReSettings _reSettings;
private readonly RulesCache _rulesCache = new RulesCache();
private readonly 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<WorkflowRules>(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<RulesEngine>();
_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<string,Func<ActionBase>> GetActionRegistry(ReSettings reSettings)
{
var actionDictionary = GetDefaultActionRegistry();
var customActions = reSettings.CustomActions ?? new Dictionary<string, Func<ActionBase>>();
foreach(var customAction in customActions){
actionDictionary.Add(customAction);
}
return actionDictionary;
}
#endregion
#region Public Methods
/// <summary>
/// This will execute all the rules of the specified workflow
/// </summary>
/// <param name="workflowName">The name of the workflow with rules to execute against the inputs</param>
/// <param name="inputs">A variable number of inputs</param>
/// <returns>List of rule results</returns>
public async ValueTask<List<RuleResultTree>> ExecuteAllRulesAsync(string workflowName, params object[] inputs)
{
_logger.LogTrace($"Called {nameof(ExecuteAllRulesAsync)} for workflow {workflowName} and count of input {inputs.Count()}");
var ruleParams = new List<RuleParameter>();
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());
}
/// <summary>
/// This will execute all the rules of the specified workflow
/// </summary>
/// <param name="workflowName">The name of the workflow with rules to execute against the inputs</param>
/// <param name="ruleParams">A variable number of rule parameters</param>
/// <returns>List of rule results</returns>
public async ValueTask<List<RuleResultTree>> ExecuteAllRulesAsync(string workflowName, params RuleParameter[] ruleParams)
{
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<ActionRuleResult> 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<ActionRuleResult> 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<RuleResultTree>() { resultTree }: null
};
}
}
#endregion
#region Private Methods
/// <summary>
/// Adds the workflow.
/// </summary>
/// <param name="workflowRules">The workflow rules.</param>
/// <exception cref="RuleValidationException"></exception>
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);
}
}
/// <summary>
/// Clears the workflows.
/// </summary>
public void ClearWorkflows()
{
_rulesCache.Clear();
}
/// <summary>
/// Removes the workflow.
/// </summary>
/// <param name="workflowNames">The workflow names.</param>
public void RemoveWorkflow(params string[] workflowNames)
{
foreach (var workflowName in workflowNames)
{
_rulesCache.Remove(workflowName);
}
}
/// <summary>
/// This will validate workflow rules then call execute method
/// </summary>
/// <typeparam name="T">type of entity</typeparam>
/// <param name="input">input</param>
/// <param name="workflowName">workflow name</param>
/// <returns>list of rule result set</returns>
private List<RuleResultTree> ValidateWorkflowAndExecuteRule(string workflowName, RuleParameter[] ruleParams)
{
List<RuleResultTree> result;
if (RegisterRule(workflowName, ruleParams))
{
result = ExecuteAllRuleByWorkflow(workflowName, ruleParams);
}
else
{
_logger.LogTrace($"Rule config file is not present for the {workflowName} workflow");
// if rules are not registered with Rules Engine
throw new ArgumentException($"Rule config file is not present for the {workflowName} workflow");
}
return result;
}
/// <summary>
/// This will compile the rules and store them to dictionary
/// </summary>
/// <param name="workflowName">workflow name</param>
/// <param name="ruleParams">The rule parameters.</param>
/// <returns>
/// bool result
/// </returns>
private bool RegisterRule(string workflowName, params RuleParameter[] ruleParams)
{
string compileRulesKey = GetCompiledRulesKey(workflowName,ruleParams);
if (_rulesCache.ContainsCompiledRules(compileRulesKey))
return true;
var workflowRules = _rulesCache.GetWorkFlowRules(workflowName);
if (workflowRules != null)
{
var dictFunc = new Dictionary<string,RuleFunc<RuleResultTree>>();
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<RuleResultTree> 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<RuleResultTree> CompileRule(string workflowName, RuleParameter[] ruleParams, Rule rule)
{
var compiledParamsKey = GetCompiledParamsCacheKey(workflowName, rule.RuleName, ruleParams);
IEnumerable<CompiledParam> compiledParamList = _compiledParamsCache.GetOrCreate(compiledParamsKey, (entry) => _ruleParamCompiler.CompileParamsExpression(rule, ruleParams));
var compiledRuleParameters = compiledParamList?.Select(c => c.AsRuleParameter()) ?? new List<RuleParameter>();
var updatedRuleParams = ruleParams?.Concat(compiledRuleParameters);
var compiledRule = _ruleCompiler.CompileRule(rule, updatedRuleParams?.ToArray());
RuleFunc<RuleResultTree> updatedRule = (RuleParameter[] paramList) =>
{
var inputs = paramList.AsEnumerable();
IEnumerable<CompiledParam> localParams = compiledParamList ?? new List<CompiledParam>();
var evaluatedParamList = new List<RuleParameter>();
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;
}
/// <summary>
/// This will execute the compiled rules
/// </summary>
/// <param name="workflowName"></param>
/// <param name="ruleParams"></param>
/// <returns>list of rule result set</returns>
private List<RuleResultTree> ExecuteAllRuleByWorkflow(string workflowName, RuleParameter[] ruleParameters)
{
_logger.LogTrace($"Compiled rules found for {workflowName} workflow and executed");
List<RuleResultTree> result = new List<RuleResultTree>();
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<string,Func<ActionBase>> GetDefaultActionRegistry(){
return new Dictionary<string, Func<ActionBase>>{
{"OutputExpression",() => new OutputExpressionAction(_ruleExpressionParser) },
{"EvaluateRule", () => new EvaluateRuleAction(this) }
};
}
/// <summary>
/// The result
/// </summary>
/// <param name="ruleResultList">The result.</param>
/// <returns>Updated error message.</returns>
private IEnumerable<RuleResultTree> FormatErrorMessages(IEnumerable<RuleResultTree> 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;
}
/// <summary>
/// Updates the error message.
/// </summary>
/// <param name="errorMessage">The error message.</param>
/// <param name="evaluatedParams">The evaluated parameters.</param>
/// <param name="property">The property.</param>
/// <param name="typeName">Name of the type.</param>
/// <param name="propertyName">Name of the property.</param>
/// <returns>Updated error message.</returns>
private static string UpdateErrorMessage(string errorMessage, IDictionary<string,object> 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
}
}

View File

@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Version>2.1.4</Version>
<Version>3.0.0-preview.1</Version>
<Copyright>Copyright (c) Microsoft Corporation.</Copyright>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://github.com/microsoft/RulesEngine</PackageProjectUrl>
@ -14,30 +14,25 @@
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<Optimize>true</Optimize>
</PropertyGroup>
<PropertyGroup Label="SourceLink">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\LICENSE" Pack="true" PackagePath="" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="9.0.1" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="System.Linq" Version="4.3.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.2.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="8.4.0" />
<PackageReference Include="Microsoft.CSharp" Version="4.5.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="System.Linq" Version="4.3.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.2.2" />
</ItemGroup>
<ItemGroup>
<None Include="..\..\..\LICENSE">
<Pack>True</Pack>
<PackagePath></PackagePath>
</None>
</ItemGroup>
</Project>

View File

@ -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
{
/// <summary>
/// This class will build the list expression
/// </summary>
internal sealed class LambdaExpressionBuilder : RuleExpressionBuilderBase
{
private readonly ReSettings _reSettings;
internal LambdaExpressionBuilder(ReSettings reSettings)
{
_reSettings = reSettings;
}
internal override Expression<Func<RuleInput, RuleResultTree>> BuildExpressionForRule(Rule rule, IEnumerable<ParameterExpression> 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);
}
}
/// <summary>Builds the expression for rule parameter.</summary>
/// <param name="param">The parameter.</param>
/// <param name="typeParamExpressions">The type parameter expressions.</param>
/// <param name="ruleInputExp">The rule input exp.</param>
/// <returns>Expression.</returns>
internal override Expression BuildExpressionForRuleParam(LocalParam param, IEnumerable<ParameterExpression> 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;
}
}
}

View File

@ -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
{
/// <summary>
/// Helpers
/// </summary>
internal static class Helpers
{
/// <summary>
/// To the result tree expression.
/// </summary>
/// <param name="rule">The rule.</param>
/// <param name="childRuleResults">The child rule results.</param>
/// <param name="isSuccessExp">The is success exp.</param>
/// <param name="typeParamExpressions">The type parameter expressions.</param>
/// <param name="ruleInputExp">The rule input exp.</param>
/// <returns>Expression of func</returns>
internal static Expression<Func<RuleInput, RuleResultTree>> ToResultTreeExpression(Rule rule, IEnumerable<MemberInitExpression> childRuleResults, BinaryExpression isSuccessExp, IEnumerable<ParameterExpression> typeParamExpressions, ParameterExpression ruleInputExp, string exceptionMessage = "")
{
var memberInit = ToResultTree(rule, childRuleResults, isSuccessExp, typeParamExpressions, null, exceptionMessage);
var lambda = Expression.Lambda<Func<RuleInput, RuleResultTree>>(memberInit, new[] { ruleInputExp });
return lambda;
}
/// <summary>
/// To the result tree member expression
/// </summary>
/// <param name="rule">The rule.</param>
/// <param name="childRuleResults">The child rule results.</param>
/// <param name="isSuccessExp">The is success exp.</param>
/// <param name="childRuleResultsblockexpr">The child rule results block expression.</param>
/// <returns></returns>
internal static MemberInitExpression ToResultTree(Rule rule, IEnumerable<MemberInitExpression> childRuleResults, BinaryExpression isSuccessExp, IEnumerable<ParameterExpression> 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;
}
/// <summary>
/// To the result tree error messages
/// </summary>
/// <param name="ruleResultTree">ruleResultTree</param>
/// <param name="ruleResultMessage">ruleResultMessage</param>
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);
}
}
}
}
/// <summary>
/// To get the child error message recersivly
/// </summary>
/// <param name="childResultTree">childResultTree</param>
/// <param name="ruleResultMessage">ruleResultMessage</param>
private static void GetChildRuleMessages(IEnumerable<RuleResultTree> childResultTree, ref RuleResultMessage ruleResultMessage)
{
foreach (var item in childResultTree)
{
ToResultTreeMessages(item, ref ruleResultMessage);
}
}
}
}

View File

@ -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
{
/// <summary>
/// Gets or sets the compiled rules.
/// </summary>
/// <value>
/// The compiled rules.
/// </value>
internal Delegate Rule { get; set; }
/// <summary>
/// Gets or sets the rule parameters.
/// </summary>
/// <value>
/// The rule parameters.
/// </value>
internal CompiledRuleParam CompiledParameters { get; set; }
}
}

View File

@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace RulesEngine.Models
{
/// <summary>Class CompiledRule.</summary>
internal class CompiledRuleParam
{
/// <summary>
/// Gets or sets the compiled rules.
/// </summary>
/// <value>
/// The compiled rules.
/// </value>
internal string Name { get; set; }
/// <summary>Gets or sets the rule parameters.</summary>
/// <value>The rule parameters.</value>
internal IEnumerable<CompiledParam> CompiledParameters { get; set; }
/// <summary>
/// Gets or sets the rule parameters.
/// </summary>
/// <value>
/// The rule parameters.
/// </value>
internal IEnumerable<RuleParameter> RuleParameters { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// Rule class
/// </summary>
[ExcludeFromCodeCoverage]
public class Rule
{
/// <summary>
/// Gets or sets the name of the rule.
/// </summary>
/// <value>
/// The name of the rule.
/// </value>
public string RuleName { get; set; }
/// <summary>
/// Gets or sets the operator.
/// </summary>
/// <value>
/// The operator.
/// </value>
public string Operator { get; set; }
/// <summary>
/// Gets or sets the error message.
/// </summary>
/// <value>
/// The error message.
/// </value>
public string ErrorMessage { get; set; }
/// <summary>
/// Gets or sets the type of the error.
/// </summary>
/// <value>
/// The type of the error.
/// </value>
[JsonConverter(typeof(StringEnumConverter))]
public ErrorType ErrorType { get; set; }
/// <summary>
/// Gets or sets the type of the rule expression.
/// </summary>
/// <value>
/// The type of the rule expression.
/// </value>
[JsonConverter(typeof(StringEnumConverter))]
public RuleExpressionType? RuleExpressionType { get; set; }
/// <summary>
/// Gets or sets the names of common workflows
/// </summary>
public List<string> WorkflowRulesToInject { get; set; }
/// <summary>
/// Gets or sets the rules.
/// </summary>
/// <value>
/// The rules.
/// </value>
public List<Rule> Rules { get; set; }
/// <summary>
/// Gets the parameters.
/// </summary>
/// <value>
/// The parameters.
/// </value>
[JsonProperty]
public IEnumerable<LocalParam> LocalParams { get; private set; }
/// <summary>
/// Gets or Sets the lambda expression.
/// </summary>
public string Expression { get; set; }
/// <summary>
/// Gets or sets the success event.
/// </summary>
/// <value>
/// The success event.
/// </value>
public string SuccessEvent { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// Rule input
/// </summary>
[ExcludeFromCodeCoverage]
internal class RuleInput
{
/// <summary>
/// Gets the today UTC.
/// </summary>
/// <value>
/// The today UTC.
/// </value>
public DateTime TodayUtc { get; set; }
}
}

View File

@ -1,65 +0,0 @@
using RulesEngine.Models;
using System;
using System.Collections.Concurrent;
using System.Linq;
namespace RulesEngine
{
/// <summary>Maintains the cache of evaludated param.</summary>
internal class ParamCache<T> where T : class
{
/// <summary>
/// The compile rules
/// </summary>
private readonly ConcurrentDictionary<string, T> _evaluatedParams = new ConcurrentDictionary<string, T>();
/// <summary>
/// <para></para>
/// <para>Determines whether the specified parameter key name contains parameters.
/// </para>
/// </summary>
/// <param name="paramKeyName">Name of the parameter key.</param>
/// <returns>
/// <c>true</c> if the specified parameter key name contains parameters; otherwise, <c>false</c>.</returns>
public bool ContainsParams(string paramKeyName)
{
return _evaluatedParams.ContainsKey(paramKeyName);
}
/// <summary>Adds the or update evaluated parameter.</summary>
/// <param name="paramKeyName">Name of the parameter key.</param>
/// <param name="ruleParameters">The rule parameters.</param>
public void AddOrUpdateParams(string paramKeyName, T ruleParameters)
{
_evaluatedParams.AddOrUpdate(paramKeyName, ruleParameters, (k, v) => v);
}
/// <summary>Clears this instance.</summary>
public void Clear()
{
_evaluatedParams.Clear();
}
/// <summary>Gets the evaluated parameters.</summary>
/// <param name="paramKeyName">Name of the parameter key.</param>
/// <returns>Delegate[].</returns>
public T GetParams(string paramKeyName)
{
return _evaluatedParams[paramKeyName];
}
/// <summary>Removes the specified workflow name.</summary>
/// <param name="workflowName">Name of the workflow.</param>
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);
}
}
}
}
}

View File

@ -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
{
/// <summary>
/// Rule param compilers
/// </summary>
internal class ParamCompiler
{
/// <summary>
/// The expression builder factory
/// </summary>
private readonly RuleExpressionBuilderFactory _expressionBuilderFactory;
/// <summary>
/// The logger
/// </summary>
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="ParamCompiler"/> class.
/// </summary>
/// <param name="expressionBuilderFactory">The expression builder factory.</param>
/// <exception cref="ArgumentNullException">expressionBuilderFactory</exception>
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;
}
/// <summary>
/// Compiles the and evaluate parameter expression.
/// </summary>
/// <param name="rule">The rule.</param>
/// <param name="ruleParams">The rule parameters.</param>
/// <returns>
/// IEnumerable&lt;RuleParameter&gt;.
/// </returns>
public CompiledRuleParam CompileParamsExpression(Rule rule, IEnumerable<RuleParameter> ruleParams)
{
CompiledRuleParam compiledRuleParam = null;
if (rule.LocalParams != null)
{
var compiledParameters = new List<CompiledParam>();
var evaluatedParameters = new List<RuleParameter>();
foreach (var param in rule.LocalParams)
{
IEnumerable<ParameterExpression> 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<ParameterExpression>(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<RuleParameter> { evaluatedParam });
evaluatedParameters.Add(evaluatedParam);
}
compiledRuleParam = new CompiledRuleParam { Name = rule.RuleName, CompiledParameters = compiledParameters, RuleParameters = evaluatedParameters };
}
return compiledRuleParam;
}
/// <summary>Evaluates the compiled parameter.</summary>
/// <param name="paramName">Name of the parameter.</param>
/// <param name="compiledParam">The compiled parameter.</param>
/// <param name="ruleParams">The rule parameters.</param>
/// <returns>RuleParameter.</returns>
public RuleParameter EvaluateCompiledParam(string paramName, Delegate compiledParam, IEnumerable<RuleParameter> ruleParams)
{
var inputs = ruleParams.Select(c => c.Value);
var result = compiledParam.DynamicInvoke(new List<object>(inputs) { new RuleInput() }.ToArray());
return new RuleParameter(paramName, result);
}
// <summary>
/// Gets the parameter expression.
/// </summary>
/// <param name="ruleParams">The types.</param>
/// <returns></returns>
/// <exception cref="ArgumentException">
/// types
/// or
/// type
/// </exception>
private IEnumerable<ParameterExpression> 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);
}
}
/// <summary>
/// Gets the expression for rule.
/// </summary>
/// <param name="param">The rule.</param>
/// <param name="typeParameterExpressions">The type parameter expressions.</param>
/// <param name="ruleInputExp">The rule input exp.</param>
/// <returns></returns>
private Expression GetExpressionForRuleParam(LocalParam param, IEnumerable<ParameterExpression> typeParameterExpressions, ParameterExpression ruleInputExp)
{
return BuildExpression(param, typeParameterExpressions, ruleInputExp);
}
/// <summary>
/// Builds the expression.
/// </summary>
/// <param name="param">The rule.</param>
/// <param name="typeParameterExpressions">The type parameter expressions.</param>
/// <param name="ruleInputExp">The rule input exp.</param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
private Expression BuildExpression(LocalParam param, IEnumerable<ParameterExpression> typeParameterExpressions, ParameterExpression ruleInputExp)
{
var ruleExpressionBuilder = _expressionBuilderFactory.RuleGetExpressionBuilder(RuleExpressionType.LambdaExpression);
var expression = ruleExpressionBuilder.BuildExpressionForRuleParam(param, typeParameterExpressions, ruleInputExp);
return expression;
}
}
}

View File

@ -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
{
/// <summary>
/// Rule compilers
/// </summary>
internal class RuleCompiler
{
/// <summary>
/// The nested operators
/// </summary>
private readonly ExpressionType[] nestedOperators = new ExpressionType[] { ExpressionType.And, ExpressionType.AndAlso, ExpressionType.Or, ExpressionType.OrElse };
/// <summary>
/// The expression builder factory
/// </summary>
private readonly RuleExpressionBuilderFactory _expressionBuilderFactory;
/// <summary>
/// The logger
/// </summary>
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="RuleCompiler"/> class.
/// </summary>
/// <param name="expressionBuilderFactory">The expression builder factory.</param>
/// <exception cref="ArgumentNullException">expressionBuilderFactory</exception>
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;
}
/// <summary>
/// Compiles the rule
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="rule"></param>
/// <param name="input"></param>
/// <param name="ruleParam"></param>
/// <returns>Compiled func delegate</returns>
public Delegate CompileRule(Rule rule,params RuleParameter[] ruleParams)
{
try
{
IEnumerable<ParameterExpression> 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<Func<RuleInput, RuleResultTree>> ruleExpression = GetExpressionForRule(rule, typeParameterExpressions, ruleInputExp);
var lambdaParameterExps = new List<ParameterExpression>(typeParameterExpressions) { ruleInputExp };
var expression = Expression.Lambda(ruleExpression.Body, lambdaParameterExps);
return expression.Compile();
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
throw;
}
}
// <summary>
/// Gets the parameter expression.
/// </summary>
/// <param name="ruleParams">The types.</param>
/// <returns></returns>
/// <exception cref="ArgumentException">
/// types
/// or
/// type
/// </exception>
private IEnumerable<ParameterExpression> 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);
}
}
/// <summary>
/// Gets the expression for rule.
/// </summary>
/// <param name="rule">The rule.</param>
/// <param name="typeParameterExpressions">The type parameter expressions.</param>
/// <param name="ruleInputExp">The rule input exp.</param>
/// <returns></returns>
private Expression<Func<RuleInput, RuleResultTree>> GetExpressionForRule(Rule rule, IEnumerable<ParameterExpression> 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);
}
}
/// <summary>
/// Builds the expression.
/// </summary>
/// <param name="rule">The rule.</param>
/// <param name="typeParameterExpressions">The type parameter expressions.</param>
/// <param name="ruleInputExp">The rule input exp.</param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
private Expression<Func<RuleInput, RuleResultTree>> BuildExpression(Rule rule, IEnumerable<ParameterExpression> 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;
}
/// <summary>
/// Builds the nested expression.
/// </summary>
/// <param name="parentRule">The parent rule.</param>
/// <param name="childRules">The child rules.</param>
/// <param name="operation">The operation.</param>
/// <param name="typeParameterExpressions">The type parameter expressions.</param>
/// <param name="ruleInputExp">The rule input exp.</param>
/// <returns>Expression of func delegate</returns>
/// <exception cref="InvalidCastException"></exception>
private Expression<Func<RuleInput, RuleResultTree>> BuildNestedExpression(Rule parentRule, ExpressionType operation, IEnumerable<ParameterExpression> typeParameterExpressions, ParameterExpression ruleInputExp)
{
List<Expression<Func<RuleInput, RuleResultTree>>> expressions = new List<Expression<Func<RuleInput, RuleResultTree>>>();
foreach (var r in parentRule.Rules)
{
expressions.Add(GetExpressionForRule(r, typeParameterExpressions, ruleInputExp));
}
List<MemberInitExpression> childRuleResultTree = new List<MemberInitExpression>();
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<Func<RuleInput, RuleResultTree>> nestedExpression = Helpers.ToResultTreeExpression(parentRule, childRuleResultTree, BinaryExpression(expressions, operation), typeParameterExpressions, ruleInputExp);
return nestedExpression;
}
/// <summary>
/// Binaries the expression.
/// </summary>
/// <param name="expressions">The expressions.</param>
/// <param name="operationType">Type of the operation.</param>
/// <returns>Binary Expression</returns>
private BinaryExpression BinaryExpression(IList<Expression<Func<RuleInput, RuleResultTree>>> 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;
}
/// <summary>
/// Resolves the is success binding.
/// </summary>
/// <param name="expression">The expression.</param>
/// <returns>Binary expression of IsSuccess prop</returns>
/// <exception cref="ArgumentNullException">expression</exception>
/// <exception cref="InvalidCastException"></exception>
/// <exception cref="NullReferenceException">
/// IsSuccess
/// or
/// IsSuccess
/// or
/// IsSuccess
/// </exception>
private BinaryExpression ResolveIsSuccessBinding(Expression<Func<RuleInput, RuleResultTree>> 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;
}
}
}

View File

@ -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
{
/// <summary>
///
/// </summary>
/// <seealso cref="RulesEngine.Interfaces.IRulesEngine" />
public class RulesEngine : IRulesEngine
{
#region Variables
/// <summary>
/// The logger
/// </summary>
private readonly ILogger _logger;
/// <summary>
/// The re settings
/// </summary>
private readonly ReSettings _reSettings;
/// <summary>
/// The rules cache
/// </summary>
private readonly RulesCache _rulesCache = new RulesCache();
/// <summary>
/// The parameters cache
/// </summary>
private readonly ParamCache<CompiledRuleParam> _compiledParamsCache = new ParamCache<CompiledRuleParam>();
/// <summary>
/// The rule parameter compiler
/// </summary>
private readonly ParamCompiler ruleParamCompiler;
/// <summary>
/// The parameter parse regex
/// </summary>
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<WorkflowRules>(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<RulesEngine>();
_reSettings = reSettings ?? new ReSettings();
ruleParamCompiler = new ParamCompiler(new RuleExpressionBuilderFactory(_reSettings), _logger);
}
#endregion
#region Public Methods
/// <summary>
/// This will execute all the rules of the specified workflow
/// </summary>
/// <param name="workflowName">The name of the workflow with rules to execute against the inputs</param>
/// <param name="inputs">A variable number of inputs</param>
/// <returns>List of rule results</returns>
public List<RuleResultTree> ExecuteRule(string workflowName, params object[] inputs)
{
_logger.LogTrace($"Called ExecuteRule for workflow {workflowName} and count of input {inputs.Count()}");
var ruleParams = new List<RuleParameter>();
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());
}
/// <summary>
/// This will execute all the rules of the specified workflow
/// </summary>
/// <param name="workflowName">The name of the workflow with rules to execute against the inputs</param>
/// <param name="ruleParams">A variable number of rule parameters</param>
/// <returns>List of rule results</returns>
public List<RuleResultTree> ExecuteRule(string workflowName, params RuleParameter[] ruleParams)
{
return ValidateWorkflowAndExecuteRule(workflowName, ruleParams);
}
#endregion
#region Private Methods
/// <summary>
/// Adds the workflow.
/// </summary>
/// <param name="workflowRules">The workflow rules.</param>
/// <exception cref="RuleValidationException"></exception>
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);
}
}
/// <summary>
/// Clears the workflows.
/// </summary>
public void ClearWorkflows()
{
_rulesCache.Clear();
}
/// <summary>
/// Removes the workflow.
/// </summary>
/// <param name="workflowNames">The workflow names.</param>
public void RemoveWorkflow(params string[] workflowNames)
{
foreach (var workflowName in workflowNames)
{
_rulesCache.Remove(workflowName);
}
}
/// <summary>
/// This will validate workflow rules then call execute method
/// </summary>
/// <typeparam name="T">type of entity</typeparam>
/// <param name="input">input</param>
/// <param name="workflowName">workflow name</param>
/// <returns>list of rule result set</returns>
private List<RuleResultTree> ValidateWorkflowAndExecuteRule(string workflowName, RuleParameter[] ruleParams)
{
List<RuleResultTree> result;
if (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;
}
/// <summary>
/// This will compile the rules and store them to dictionary
/// </summary>
/// <param name="workflowName">workflow name</param>
/// <param name="ruleParams">The rule parameters.</param>
/// <returns>
/// bool result
/// </returns>
private bool 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<CompiledRule>();
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();
}
/// <summary>
/// This will execute the compiled rules
/// </summary>
/// <param name="workflowName"></param>
/// <param name="ruleParams"></param>
/// <returns>list of rule result set</returns>
private List<RuleResultTree> ExecuteRuleByWorkflow(string workflowName, RuleParameter[] ruleParameters)
{
_logger.LogTrace($"Compiled rules found for {workflowName} workflow and executed");
List<RuleResultTree> result = new List<RuleResultTree>();
string compileRulesKey = GetCompiledRulesKey(workflowName,ruleParameters);
foreach (var compiledRule in _rulesCache.GetCompiledRules(compileRulesKey))
{
IEnumerable<RuleParameter> evaluatedRuleParams = new List<RuleParameter>(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<RuleParameter> { evaluatedParam });
}
}
var inputs = evaluatedRuleParams.Select(c => c.Value);
var resultTree = compiledRule.Rule.DynamicInvoke(new List<object>(inputs) { new RuleInput() }.ToArray()) as RuleResultTree;
resultTree.RuleEvaluatedParams = evaluatedRuleParams;
result.Add(resultTree);
}
FormatErrorMessages(result?.Where(r => !r.IsSuccess));
return result;
}
/// <summary>
/// The result
/// </summary>
/// <param name="result">The result.</param>
/// <returns>Updated error message.</returns>
private IEnumerable<RuleResultTree> FormatErrorMessages(IEnumerable<RuleResultTree> 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;
}
/// <summary>
/// Updates the error message.
/// </summary>
/// <param name="errorMessage">The error message.</param>
/// <param name="evaluatedParams">The evaluated parameters.</param>
/// <param name="property">The property.</param>
/// <param name="typeName">Name of the type.</param>
/// <param name="propertyName">Name of the property.</param>
/// <returns>Updated error message.</returns>
private static string UpdateErrorMessage(string errorMessage, IEnumerable<RuleParameter> 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
}
}

View File

@ -1,7 +0,0 @@
{
"sdk": {
"version": "3.1",
"rollForward": "latestFeature",
"allowPrerelease": false
}
}

View File

@ -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<string>();
var context = new Dictionary<string, object> {
{ 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<string>();
var context = new Dictionary<string, object> {
{ nameof(contextInput), contextInput }
};
var parentRuleResult = new RuleResultTree();
var actionContext = new ActionContext(context, parentRuleResult);
string name = nameof(contextInput);
// Act
var result = actionContext.GetContext<string>(name);
// Assert
Assert.Equal(contextInput, result);
}
[Fact]
public void GetContext_ObjectContext_ReturnsTypedContext()
{
// Arrange
var fixture = new Fixture();
var contextInput = fixture.CreateMany<string>();
var context = new Dictionary<string, object> {
{ nameof(contextInput), contextInput }
};
var parentRuleResult = new RuleResultTree();
var actionContext = new ActionContext(context, parentRuleResult);
string name = nameof(contextInput);
// Act
var result = actionContext.GetContext<List<string>>(name);
// Assert
Assert.Equal(contextInput, result);
}
[Fact]
public void GetContext_ValidNameWithStringCaseDiffernce_ReturnsContext()
{
// Arrange
var fixture = new Fixture();
var contextInput = fixture.Create<string>();
var context = new Dictionary<string, object> {
{ nameof(contextInput), contextInput }
};
var parentRuleResult = new RuleResultTree();
var actionContext = new ActionContext(context, parentRuleResult);
string name = nameof(contextInput).ToUpper();
// Act
var result = actionContext.GetContext<string>(name);
// Assert
Assert.Equal(contextInput, result);
}
[Fact]
public void GetContext_InvalidName_ThrowsArgumentException()
{
// Arrange
var fixture = new Fixture();
var contextInput = fixture.Create<string>();
var context = new Dictionary<string, object> {
{ nameof(contextInput), contextInput }
};
var parentRuleResult = new RuleResultTree();
var actionContext = new ActionContext(context, parentRuleResult);
string name = fixture.Create<string>();
// Act
Assert.Throws<ArgumentException>(() => actionContext.GetContext<string>(name));
}
[Fact]
public void GetContext_PrimitiveInputs_ReturnsResult()
{
// Arrange
var fixture = new Fixture();
var intInput = fixture.Create<int>();
var strInput = fixture.Create<string>();
var floatInput = fixture.Create<float>();
var context = new Dictionary<string, object> {
{ nameof(intInput), intInput },
{ nameof(strInput), strInput },
{ nameof(floatInput), floatInput },
};
var parentRuleResult = new RuleResultTree();
var actionContext = new ActionContext(context, parentRuleResult);
// Act
var intResult = actionContext.GetContext<int>(nameof(intInput));
var strResult = actionContext.GetContext<string>(nameof(strInput));
var floatResult = actionContext.GetContext<float>(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<string>();
var context = new Dictionary<string, object> {
{ nameof(contextInput), contextInput }
};
var parentRuleResult = new RuleResultTree();
var actionContext = new ActionContext(context, parentRuleResult);
string name = fixture.Create<string>();
// Act
Assert.Throws<ArgumentException>(() => actionContext.GetContext<List<string>>(name));
}
[Fact]
public void GetContext_InvalidTypeConversion_ThrowsArgumentException()
{
// Arrange
var fixture = new Fixture();
var contextInput = fixture.CreateMany<string>();
var context = new Dictionary<string, object> {
{ nameof(contextInput), contextInput }
};
var parentRuleResult = new RuleResultTree();
var actionContext = new ActionContext(context, parentRuleResult);
string name = nameof(contextInput);
// Act
Assert.Throws<ArgumentException>(() => actionContext.GetContext<RuleResultTree>(name));
}
}
}

View File

@ -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<ArgumentException>(async () => await engine.ExecuteActionWorkflowAsync("WrongWorkflow", "ExpressionOutputRuleTest", new RuleParameter[0]));
await Assert.ThrowsAsync<ArgumentException>(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<Rule>{
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<Rule>{
new Rule{
RuleName = "ExpressionOutputRuleTest",
RuleExpressionType = RuleExpressionType.LambdaExpression,
Expression = "1 == 1",
Actions = new Dictionary<ActionTriggerType, ActionInfo>{
{ ActionTriggerType.onSuccess, new ActionInfo{
Name = "OutputExpression",
Context = new Dictionary<string, object>{
{"expression", "2*2"}
}
}}
}
},
new Rule{
RuleName = "EvaluateRuleTest",
RuleExpressionType = RuleExpressionType.LambdaExpression,
Expression = "1 == 1",
Actions = new Dictionary<ActionTriggerType, ActionInfo>{
{ ActionTriggerType.onSuccess, new ActionInfo{
Name = "EvaluateRule",
Context = new Dictionary<string, object>{
{"workflowName", "ActionWorkflow"},
{"ruleName","ExpressionOutputRuleTest"}
}
}}
}
}
}
};
return new []{workflow1};
}
}
}

View File

@ -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<List<RuleResultTree>>(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<RuleResultTree> result = re.ExecuteRule("inputWorkflow", input1, input2, input3);
List<RuleResultTree> result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3);
Assert.NotNull(result);
Assert.IsType<List<RuleResultTree>>(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<RuleResultTree> result = re.ExecuteRule("inputWorkflow", input1);
List<RuleResultTree> result1 = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3);
Assert.NotNull(result1);
Assert.IsType<List<RuleResultTree>>(result1);
Assert.Contains(result1, c => c.IsSuccess);
List<RuleResultTree> result2 = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3);
Assert.NotNull(result2);
Assert.IsType<List<RuleResultTree>>(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<RuleResultTree> result = await re.ExecuteAllRulesAsync("inputWorkflow", input1);
Assert.NotNull(result);
Assert.IsType<List<RuleResultTree>>(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<RuleResultTree> result = re.ExecuteRule("inputWorkflow", input1, input2, input3);
List<RuleResultTree> 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<RuleResultTree> result = re.ExecuteRule("inputWorkflow", input1, input2, input3);
List<RuleResultTree> 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<ArgumentException>(() => { re.ExecuteRule("inputWorkflow1", input); });
await Assert.ThrowsAsync<ArgumentException>(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<ArgumentException>(() => re.ExecuteRule("inputWorkflow", input1, input2, input3));
var result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3);
Assert.NotNull(result);
re.RemoveWorkflow("inputWorkflow");
await Assert.ThrowsAsync<ArgumentException>(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<ArgumentException>(() => re.ExecuteRule("inputWorkflow", input1, input2, input3));
Assert.Throws<ArgumentException>(() => re.ExecuteRule("inputWorkflowReference", input1, input2, input3));
await Assert.ThrowsAsync<ArgumentException>(async() => await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3));
await Assert.ThrowsAsync<ArgumentException>(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<RuleResultTree> result = re.ExecuteRule("inputWorkflow", input1, input2, input3);
List<RuleResultTree> result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3);
Assert.NotNull(result);
Assert.IsType<List<RuleResultTree>>(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<List<RuleResultTree>>(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<WorkflowRules[]>(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<RuleResultTree> result = re.ExecuteRule("inputWorkflow", input1, input2, input3);
List<RuleResultTree> result = await re.ExecuteAllRulesAsync("inputWorkflow", input1,input2, input3);
Assert.NotNull(result);
Assert.IsType<List<RuleResultTree>>(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<RuleResultTree> result = re.ExecuteRule("inputWorkflow", new RuleParameter("input1", input1), new RuleParameter("utils", utils));
List<RuleResultTree> result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1",input1),new RuleParameter("utils",utils));
Assert.NotNull(result);
Assert.IsType<List<RuleResultTree>>(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<bool> 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<RuleResultTree> result = re.ExecuteRule("inputWorkflow", new RuleParameter("input1", input1));
List<RuleResultTree> result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1));
Assert.NotNull(result);
Assert.IsType<List<RuleResultTree>>(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<RuleResultTree> result = re.ExecuteRule("inputWorkflow", new RuleParameter("input1", input1));
List<RuleResultTree> result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1));
Assert.NotNull(result);
Assert.IsType<List<RuleResultTree>>(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<RuleResultTree> result = re.ExecuteRule("inputWorkflow", new RuleParameter("input1", input1));
List<RuleResultTree> result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1));
Assert.NotNull(result);
Assert.IsType<List<RuleResultTree>>(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<System.Linq.Dynamic.Core.Exceptions.ParseException>(()=>
{
List<RuleResultTree> result = re.ExecuteRule("inputWorkflow", new RuleParameter("input1", input1));
await Assert.ThrowsAsync<System.Linq.Dynamic.Core.Exceptions.ParseException>(async()=>
{
List<RuleResultTree> 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<RuleResultTree> result = re.ExecuteRule("inputWorkflow", new RuleParameter("input1", input1));
List<RuleResultTree> result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1));
Assert.NotNull(result);
Assert.IsType<List<RuleResultTree>>(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<List<RuleTestClass>>(laborCategoriesInput);
dynamic input2 = JsonConvert.DeserializeObject<ExpandoObject>(currentLaborCategoryInput, converter);
dynamic input3 = JsonConvert.DeserializeObject<ExpandoObject>(telemetryInfo, converter);
dynamic input4 = JsonConvert.DeserializeObject<ExpandoObject>(basicInfo, converter);
dynamic input5 = JsonConvert.DeserializeObject<ExpandoObject>(orderInfo, converter);
dynamic input2 = JsonConvert.DeserializeObject<ExpandoObject>(currentLaborCategoryInput);
dynamic input3 = JsonConvert.DeserializeObject<ExpandoObject>(telemetryInfo);
dynamic input4 = JsonConvert.DeserializeObject<ExpandoObject>(basicInfo);
dynamic input5 = JsonConvert.DeserializeObject<ExpandoObject>(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;
}
}
}
}

View File

@ -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()

View File

@ -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]

View File

@ -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<ParameterExpression>();
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);
}
}
}

View File

@ -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<string, object>(),
IsSuccess = true,
Rule = new Rule()
{
@ -30,7 +32,7 @@ namespace RulesEngine.UnitTest
{
ChildResults = null,
ExceptionMessage = string.Empty,
Input = new object(),
Inputs = new Dictionary<string, object>(),
IsSuccess = false,
Rule = new Rule()
{
@ -59,7 +61,7 @@ namespace RulesEngine.UnitTest
{
ChildResults = null,
ExceptionMessage = string.Empty,
Input = new object(),
Inputs = new Dictionary<string, object>(),
IsSuccess = true,
Rule = new Rule()
{
@ -71,7 +73,7 @@ namespace RulesEngine.UnitTest
{
ChildResults = null,
ExceptionMessage = string.Empty,
Input = new object(),
Inputs = new Dictionary<string, object>(),
IsSuccess = false,
Rule = new Rule()
{
@ -100,7 +102,7 @@ namespace RulesEngine.UnitTest
{
ChildResults = null,
ExceptionMessage = string.Empty,
Input = new object(),
Inputs = new Dictionary<string, object>(),
IsSuccess = false,
Rule = new Rule()
{
@ -111,7 +113,7 @@ namespace RulesEngine.UnitTest
{
ChildResults = null,
ExceptionMessage = string.Empty,
Input = new object(),
Inputs = new Dictionary<string, object>(),
IsSuccess = false,
Rule = new Rule()
{
@ -141,7 +143,7 @@ namespace RulesEngine.UnitTest
{
ChildResults = null,
ExceptionMessage = string.Empty,
Input = new object(),
Inputs = new Dictionary<string, object>(),
IsSuccess = true,
Rule = new Rule()
{
@ -152,7 +154,7 @@ namespace RulesEngine.UnitTest
{
ChildResults = null,
ExceptionMessage = string.Empty,
Input = new object(),
Inputs = new Dictionary<string, object>(),
IsSuccess = false,
Rule = new Rule()
{
@ -181,7 +183,7 @@ namespace RulesEngine.UnitTest
{
ChildResults = null,
ExceptionMessage = string.Empty,
Input = new object(),
Inputs = new Dictionary<string, object>(),
IsSuccess = false,
Rule = new Rule()
{
@ -192,7 +194,7 @@ namespace RulesEngine.UnitTest
{
ChildResults = null,
ExceptionMessage = string.Empty,
Input = new object(),
Inputs = new Dictionary<string, object>(),
IsSuccess = false,
Rule = new Rule()
{

View File

@ -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
{
/// <summary>Class PrivateSetterContractResolver.
/// Implements the <see cref="Newtonsoft.Json.Serialization.DefaultContractResolver" /></summary>
public class PrivateSetterContractResolver : DefaultContractResolver
{
/// <summary>Creates a <see cref="T:Newtonsoft.Json.Serialization.JsonProperty" /> for the given <see cref="T:System.Reflection.MemberInfo">MemberInfo</see>.</summary>
/// <param name="member">The member to create a <see cref="T:Newtonsoft.Json.Serialization.JsonProperty" /> for.</param>
/// <param name="memberSerialization">The member's parent <see cref="T:Newtonsoft.Json.MemberSerialization" />.</param>
/// <returns>A created <see cref="T:Newtonsoft.Json.Serialization.JsonProperty" /> for the given <see cref="T:System.Reflection.MemberInfo">MemberInfo</see>.</returns>
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;
}
}
}

View File

@ -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<ArgumentNullException>(() => new RuleCompiler(null, null));
Assert.Throws<ArgumentNullException>(() => new RuleCompiler(new RuleExpressionBuilderFactory(new ReSettings()), null));
var reSettings = new ReSettings();
var parser = new RuleExpressionParser(reSettings);
Assert.Throws<ArgumentNullException>(() => new RuleCompiler(new RuleExpressionBuilderFactory(reSettings,parser), null));
}
[Fact]
public void RuleCompiler_CompileRule_ThrowsException()
{
var compiler = new RuleCompiler(new RuleExpressionBuilderFactory(new ReSettings()), new NullLogger<RuleCompiler>());
Assert.Throws<ArgumentException>(() => compiler.CompileRule(null, null));
Assert.Throws<ArgumentException>(() => 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<RuleCompiler>());
Assert.Throws<ArgumentNullException>(() => compiler.CompileRule(null, null));
Assert.Throws<ArgumentNullException>(() => compiler.CompileRule(null, new RuleParameter[] { null}));
}

View File

@ -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();

View File

@ -1,40 +1,19 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text;
namespace RulesEngine.UnitTest
{
/// <summary>
/// Class RuleTestClass.
/// </summary>
[ExcludeFromCodeCoverage]
public class RuleTestClass
{
/// <summary>
/// Gets the country.
/// </summary>
/// <value>
/// The country.
/// </value>
[JsonProperty("country")]
public string Country { get; private set; }
public string Country { get; set; }
/// <summary>
/// Gets the loyality factor.
/// </summary>
/// <value>
/// The loyality factor.
/// </value>
[JsonProperty("loyalityFactor")]
public int LoyalityFactor { get; private set; }
/// <summary>
/// Gets the total purchases to date.
/// </summary>
/// <value>
/// The total purchases to date.
/// </value>
[JsonProperty("totalPurchasesToDate")]
public int TotalPurchasesToDate { get; private set; }
public int LoyalityFactor { get; set; }
public int TotalPurchasesToDate { get; set; }
}
}

View File

@ -1,25 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="Moq" Version="4.12.0" />
<PackageReference Include="AutoFixture" Version="4.14.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="Moq" Version="4.14.5" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="1.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\RulesEngine\RulesEngine\RulesEngine.csproj" />
<ProjectReference Include="..\..\src\RulesEngine\RulesEngine.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="TestData\rules1.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@ -33,21 +29,20 @@
<None Update="TestData\rules2.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestData\rules9.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestData\rules8.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestData\rules7.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestData\rules6.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestData\rules5.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestData\rules6.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestData\rules7.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestData\rules8.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestData\rules9.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
</Project>

View File

@ -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"
},

View File

@ -10,4 +10,4 @@
"Expression": "utils.CheckExists(String(input1.Property1)) == true"
}
]
}
}

View File

@ -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()"
}
]
}

View File

@ -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"
}
]
}

View File

@ -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)"
}
]
}

View File

@ -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()"
}
]
}
"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()"
}
]
}

View File

@ -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<ExpandoObject>(typedobj);
Assert.NotNull(typedobj.GetType().GetProperty("test"));
Console.WriteLine($"{typedobj.GetType()} & {typedobj2.GetType()}");
Assert.Equal(typedobj.GetType(),typedobj2.GetType());
}