Abbasc52/combined fixes (#341)

* Fixed actions not working with complex objects

* removed Microsoft.Extensions.Caching dependency

* * updated nuget packages
* update dotnet version to 6 for demo/test proj
* improved integration with DynamicLinq

* added test case for jsonElement getProperty method

* updated test cases to cover case insensitivity

* updated workflow schema

* added schema for list of workflows

* fixed schema name and added to solution

* Update dotnetcore-build.yml

* Update dotnetcore-build.yml

* updated unit test proj to point to dotnet 6

* removed ilogger
pull/342/head
Abbas Cyclewala 2022-04-12 19:24:16 +05:30 committed by GitHub
parent 99bad9ffff
commit 571490455c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1603 additions and 1403 deletions

View File

@ -3,7 +3,7 @@ updates:
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
# default location of `.github/workflows` # default location of `.github/workflows`
directory: "/" directory: "/"
open-pull-requests-limit: 10 open-pull-requests-limit: 3
schedule: schedule:
interval: "weekly" interval: "weekly"
# assignees: # assignees:
@ -14,9 +14,9 @@ updates:
- package-ecosystem: "nuget" - package-ecosystem: "nuget"
# location of package manifests # location of package manifests
directory: "/" directory: "/"
open-pull-requests-limit: 10 open-pull-requests-limit: 3
schedule: schedule:
interval: "daily" interval: "weekly"
ignore: ignore:
- dependency-name: "*" - dependency-name: "*"
update-types: ["version-update:semver-minor"] update-types: ["version-update:semver-minor"]

View File

@ -31,12 +31,12 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Setup .NET Core - name: Setup .NET Core
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v2
with: with:
dotnet-version: 3.1 dotnet-version: 6.0.x
- name: Install minicover - name: Install minicover
run: dotnet tool install --global minicover --version 3.0.6 run: dotnet tool install --global minicover --version 3.4.4
- name: Install dependencies - name: Install dependencies
run: dotnet restore RulesEngine.sln run: dotnet restore RulesEngine.sln

View File

@ -18,6 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
CHANGELOG.md = CHANGELOG.md CHANGELOG.md = CHANGELOG.md
global.json = global.json global.json = global.json
README.md = README.md README.md = README.md
schema\workflow-list-schema.json = schema\workflow-list-schema.json
schema\workflow-schema.json = schema\workflow-schema.json schema\workflow-schema.json = schema\workflow-schema.json
EndProjectSection EndProjectSection
EndProject EndProject

View File

@ -36,7 +36,7 @@ namespace RulesEngineBenchmark
var fileData = File.ReadAllText(files[0]); var fileData = File.ReadAllText(files[0]);
workflow = JsonConvert.DeserializeObject<List<Workflow>>(fileData); workflow = JsonConvert.DeserializeObject<List<Workflow>>(fileData);
rulesEngine = new RulesEngine.RulesEngine(workflow.ToArray(), null, new ReSettings { rulesEngine = new RulesEngine.RulesEngine(workflow.ToArray(), new ReSettings {
EnableFormattedErrorMessage = false, EnableFormattedErrorMessage = false,
EnableScopedParams = false EnableScopedParams = false
}); });

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>net6.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<RootNamespace>DemoApp.EFDataExample</RootNamespace> <RootNamespace>DemoApp.EFDataExample</RootNamespace>
<AssemblyName>DemoApp.EFDataExample</AssemblyName> <AssemblyName>DemoApp.EFDataExample</AssemblyName>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -24,18 +24,20 @@ namespace RulesEngine.Data
entity.Ignore(b => b.WorkflowsToInject); entity.Ignore(b => b.WorkflowsToInject);
}); });
var serializationOptions = new JsonSerializerOptions(JsonSerializerDefaults.General);
modelBuilder.Entity<Rule>(entity => { modelBuilder.Entity<Rule>(entity => {
entity.HasKey(k => k.RuleName); entity.HasKey(k => k.RuleName);
entity.Property(b => b.Properties) entity.Property(b => b.Properties)
.HasConversion( .HasConversion(
v => JsonSerializer.Serialize(v, null), v => JsonSerializer.Serialize(v, serializationOptions),
v => JsonSerializer.Deserialize<Dictionary<string, object>>(v, null)); v => JsonSerializer.Deserialize<Dictionary<string, object>>(v, serializationOptions)); ;
entity.Property(p => p.Actions) entity.Property(p => p.Actions)
.HasConversion( .HasConversion(
v => JsonSerializer.Serialize(v, null), v => JsonSerializer.Serialize(v, serializationOptions),
v => JsonSerializer.Deserialize<RuleActions>(v, null)); v => JsonSerializer.Deserialize<RuleActions>(v, serializationOptions));
entity.Ignore(b => b.WorkflowsToInject); entity.Ignore(b => b.WorkflowsToInject);
}); });

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<StartupObject>DemoApp.Program</StartupObject> <StartupObject>DemoApp.Program</StartupObject>
</PropertyGroup> </PropertyGroup>

View File

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

View File

@ -0,0 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "array",
"items": {
"$ref": "https://raw.githubusercontent.com/microsoft/RulesEngine/main/schema/workflow-schema.json"
}
}

View File

@ -1,12 +1,24 @@
{ {
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"definitions": { "definitions": {
"ScopedParam": {
"type": "object",
"properties": {
"Name": { "type": "string" },
"Expression": { "type": "string" }
},
"required": [ "Name", "Expression" ]
},
"Rule": { "Rule": {
"title": "Rule", "title": "Rule",
"properties": { "properties": {
"RuleName": { "RuleName": {
"type": "string" "type": "string"
}, },
"LocalParams": {
"type": "array",
"items": { "$ref": "#/definitions/ScopedParam" }
},
"Operator": { "Operator": {
"enum": [ "enum": [
"And", "And",
@ -18,12 +30,6 @@
"ErrorMessage": { "ErrorMessage": {
"type": "string" "type": "string"
}, },
"ErrorType": {
"enum": [
"Warning",
"Error"
]
},
"SuccessEvent": { "SuccessEvent": {
"type": "string" "type": "string"
}, },
@ -45,6 +51,10 @@
}, },
"Actions": { "Actions": {
"$ref": "#/definitions/RuleActions" "$ref": "#/definitions/RuleActions"
},
"Enabled": {
"type": "boolean",
"default": true
} }
}, },
"required": [ "required": [
@ -65,6 +75,10 @@
"RuleName": { "RuleName": {
"type": "string" "type": "string"
}, },
"LocalParams": {
"type": "array",
"items": { "$ref": "#/definitions/ScopedParam" }
},
"Expression": { "Expression": {
"type": "string" "type": "string"
}, },
@ -76,12 +90,6 @@
"ErrorMessage": { "ErrorMessage": {
"type": "string" "type": "string"
}, },
"ErrorType": {
"enum": [
"Warning",
"Error"
]
},
"SuccessEvent": { "SuccessEvent": {
"type": "string" "type": "string"
}, },
@ -90,6 +98,10 @@
}, },
"Actions": { "Actions": {
"$ref": "#/definitions/RuleActions" "$ref": "#/definitions/RuleActions"
},
"Enabled": {
"type": "boolean",
"default": true
} }
} }
}, },
@ -116,11 +128,20 @@
} }
} }
} }
}, },
"properties": { "properties": {
"WorkFlowName": { "WorkflowName": {
"type": "string" "type": "string"
}, },
"WorkflowsToInject": {
"type": "array",
"items": { "type": "string" }
},
"GlobalParams": {
"type": "array",
"items": { "$ref": "#/definitions/ScopedParam" }
},
"Rules": { "Rules": {
"type": "array", "type": "array",
"items": { "items": {
@ -136,7 +157,7 @@
} }
}, },
"required": [ "required": [
"WorkFlowName", "WorkflowName",
"Rules" "Rules"
], ],
"type": "object" "type": "object"

View File

@ -43,7 +43,7 @@ namespace RulesEngine.ExpressionBuilders
} }
} }
internal override LambdaExpression Parse(string expression, ParameterExpression[] parameters, Type returnType) internal override Expression Parse(string expression, ParameterExpression[] parameters, Type returnType)
{ {
try try
{ {

View File

@ -22,7 +22,7 @@ namespace RulesEngine.ExpressionBuilders
/// <returns>Expression type</returns> /// <returns>Expression type</returns>
internal abstract RuleFunc<RuleResultTree> BuildDelegateForRule(Rule rule, RuleParameter[] ruleParams); internal abstract RuleFunc<RuleResultTree> BuildDelegateForRule(Rule rule, RuleParameter[] ruleParams);
internal abstract LambdaExpression Parse(string expression, ParameterExpression[] parameters, Type returnType); internal abstract Expression Parse(string expression, ParameterExpression[] parameters, Type returnType);
internal abstract Func<object[], Dictionary<string, object>> CompileScopedParams(RuleParameter[] ruleParameters, RuleExpressionParameter[] scopedParameters); internal abstract Func<object[], Dictionary<string, object>> CompileScopedParams(RuleParameter[] ruleParameters, RuleExpressionParameter[] scopedParameters);
} }

View File

@ -2,12 +2,13 @@
// Licensed under the MIT License. // Licensed under the MIT License.
using FastExpressionCompiler; using FastExpressionCompiler;
using Microsoft.Extensions.Caching.Memory; using RulesEngine.HelperFunctions;
using RulesEngine.Models; using RulesEngine.Models;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Dynamic.Core; using System.Linq.Dynamic.Core;
using System.Linq.Dynamic.Core.Parser;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
@ -16,13 +17,13 @@ namespace RulesEngine.ExpressionBuilders
public class RuleExpressionParser public class RuleExpressionParser
{ {
private readonly ReSettings _reSettings; private readonly ReSettings _reSettings;
private static IMemoryCache _memoryCache; private static MemCache _memoryCache;
private readonly IDictionary<string, MethodInfo> _methodInfo; private readonly IDictionary<string, MethodInfo> _methodInfo;
public RuleExpressionParser(ReSettings reSettings) public RuleExpressionParser(ReSettings reSettings)
{ {
_reSettings = reSettings; _reSettings = reSettings;
_memoryCache = _memoryCache ?? new MemoryCache(new MemoryCacheOptions { _memoryCache = _memoryCache ?? new MemCache(new MemCacheConfig {
SizeLimit = 1000 SizeLimit = 1000
}); });
_methodInfo = new Dictionary<string, MethodInfo>(); _methodInfo = new Dictionary<string, MethodInfo>();
@ -34,22 +35,30 @@ namespace RulesEngine.ExpressionBuilders
var dict_add = typeof(Dictionary<string, object>).GetMethod("Add", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(string), typeof(object) }, null); var dict_add = typeof(Dictionary<string, object>).GetMethod("Add", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(string), typeof(object) }, null);
_methodInfo.Add("dict_add", dict_add); _methodInfo.Add("dict_add", dict_add);
} }
public LambdaExpression Parse(string expression, ParameterExpression[] parameters, Type returnType) public Expression Parse(string expression, ParameterExpression[] parameters, Type returnType)
{ {
var config = new ParsingConfig { CustomTypeProvider = new CustomTypeProvider(_reSettings.CustomTypes) }; var config = new ParsingConfig { CustomTypeProvider = new CustomTypeProvider(_reSettings.CustomTypes) };
return new ExpressionParser(parameters, expression, new object[] { }, config).Parse(returnType);
return DynamicExpressionParser.ParseLambda(config, false, parameters, returnType, expression);
} }
public Func<object[], T> Compile<T>(string expression, RuleParameter[] ruleParams) public Func<object[], T> Compile<T>(string expression, RuleParameter[] ruleParams)
{ {
var rtype = typeof(T);
if(rtype == typeof(object))
{
rtype = null;
}
var cacheKey = GetCacheKey(expression, ruleParams, typeof(T)); var cacheKey = GetCacheKey(expression, ruleParams, typeof(T));
return _memoryCache.GetOrCreate(cacheKey, (entry) => { return _memoryCache.GetOrCreate(cacheKey, () => {
entry.SetSize(1);
var parameterExpressions = GetParameterExpression(ruleParams).ToArray(); var parameterExpressions = GetParameterExpression(ruleParams).ToArray();
var e = Parse(expression, parameterExpressions, typeof(T)); var e = Parse(expression, parameterExpressions, rtype);
var expressionBody = new List<Expression>() { e.Body }; if(rtype == null)
{
e = Expression.Convert(e, typeof(T));
}
var expressionBody = new List<Expression>() { e };
var wrappedExpression = WrapExpression<T>(expressionBody, parameterExpressions, new ParameterExpression[] { }); var wrappedExpression = WrapExpression<T>(expressionBody, parameterExpressions, new ParameterExpression[] { });
return wrappedExpression.CompileFast(); return wrappedExpression.CompileFast();
}); });
@ -75,7 +84,7 @@ namespace RulesEngine.ExpressionBuilders
} }
public T Evaluate<T>(string expression, RuleParameter[] ruleParams) public T Evaluate<T>(string expression, RuleParameter[] ruleParams)
{ {
var func = Compile<T>(expression, ruleParams); var func = Compile<T>(expression, ruleParams);
return func(ruleParams.Select(c => c.Value).ToArray()); return func(ruleParams.Select(c => c.Value).ToArray());
} }

View File

@ -10,7 +10,7 @@ namespace RulesEngine.HelperFunctions
{ {
public static bool CheckContains(string check, string valList) public static bool CheckContains(string check, string valList)
{ {
if (String.IsNullOrEmpty(check) || String.IsNullOrEmpty(valList)) if (string.IsNullOrEmpty(check) || string.IsNullOrEmpty(valList))
return false; return false;
var list = valList.Split(',').ToList(); var list = valList.Split(',').ToList();

View File

@ -0,0 +1,115 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
namespace RulesEngine.HelperFunctions
{
public class MemCacheConfig {
public int SizeLimit { get; set; } = 1000;
}
internal class MemCache
{
private readonly MemCacheConfig _config;
private ConcurrentDictionary<string, (object value, DateTimeOffset expiry)> _cacheDictionary;
private ConcurrentQueue<(string key, DateTimeOffset expiry)> _cacheEvictionQueue;
public MemCache(MemCacheConfig config)
{
if(config == null)
{
config = new MemCacheConfig();
}
_config = config;
_cacheDictionary = new ConcurrentDictionary<string, (object value, DateTimeOffset expiry)>();
_cacheEvictionQueue = new ConcurrentQueue<(string key, DateTimeOffset expiry)>();
}
public bool TryGetValue<T>(string key,out T value)
{
value = default;
if (_cacheDictionary.TryGetValue(key, out var cacheItem))
{
if(cacheItem.expiry < DateTimeOffset.UtcNow)
{
_cacheDictionary.TryRemove(key, out _);
return false;
}
else
{
value = (T)cacheItem.value;
return true;
}
}
return false;
}
public T Get<T>(string key)
{
TryGetValue<T>(key, out var value);
return value;
}
/// <summary>
/// Returns all known keys. May return keys for expired data as well
/// </summary>
/// <returns></returns>
public IEnumerable<string> GetKeys()
{
return _cacheDictionary.Keys;
}
public T GetOrCreate<T>(string key, Func<T> createFn, DateTimeOffset? expiry = null)
{
if(!TryGetValue<T>(key,out var value))
{
value = createFn();
return Set<T>(key,value,expiry);
}
return value;
}
public T Set<T>(string key, T value, DateTimeOffset? expiry = null)
{
var fixedExpiry = expiry ?? DateTimeOffset.MaxValue;
while (_cacheDictionary.Count > _config.SizeLimit)
{
if (_cacheEvictionQueue.IsEmpty)
{
_cacheDictionary.Clear();
}
if(_cacheEvictionQueue.TryDequeue(out var result)
&& _cacheDictionary.TryGetValue(result.key,out var dictionaryValue)
&& dictionaryValue.expiry == result.expiry)
{
_cacheDictionary.TryRemove(result.key, out _);
}
}
_cacheDictionary.AddOrUpdate(key, (value, fixedExpiry), (k, v) => (value, fixedExpiry));
_cacheEvictionQueue.Enqueue((key, fixedExpiry));
return value;
}
public void Remove(string key)
{
_cacheDictionary.TryRemove(key, out _);
}
public void Clear()
{
_cacheDictionary.Clear();
_cacheEvictionQueue = new ConcurrentQueue<(string key, DateTimeOffset expiry)>();
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the MIT License. // Licensed under the MIT License.
using RulesEngine.Actions; using RulesEngine.Actions;
using RulesEngine.HelperFunctions;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@ -57,6 +58,8 @@ namespace RulesEngine.Models
get { return EnableScopedParams; } get { return EnableScopedParams; }
set { EnableScopedParams = value; } set { EnableScopedParams = value; }
} }
public MemCacheConfig CacheConfig { get; set; }
} }
public enum NestedRuleExecutionMode public enum NestedRuleExecutionMode

View File

@ -23,13 +23,13 @@ namespace RulesEngine.Models
/// </summary> /// </summary>
public string WorkflowName { get; set; } public string WorkflowName { get; set; }
/// <summary>Gets or sets the workflow rules to inject.</summary> /// <summary>Gets or sets the workflow rules to inject.</summary>
/// <value>The workflow rules to inject.</value> /// <value>The workflow rules to inject.</value>
[Obsolete("WorkflowRulesToInject is deprecated. Use WorkflowsToInject instead.")] [Obsolete("WorkflowRulesToInject is deprecated. Use WorkflowsToInject instead.")]
public IEnumerable<string> WorkflowRulesToInject { public IEnumerable<string> WorkflowRulesToInject {
set { WorkflowsToInject = value; } set { WorkflowsToInject = value; }
} }
public IEnumerable<string> WorkflowsToInject { get; set; } public IEnumerable<string> WorkflowsToInject { get; set; }
/// <summary> /// <summary>
/// Gets or Sets the global params which will be applicable to all rules /// Gets or Sets the global params which will be applicable to all rules

View File

@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation. // Copyright (c) Microsoft Corporation.
// Licensed under the MIT License. // Licensed under the MIT License.
using Microsoft.Extensions.Logging;
using RulesEngine.Exceptions; using RulesEngine.Exceptions;
using RulesEngine.ExpressionBuilders; using RulesEngine.ExpressionBuilders;
using RulesEngine.HelperFunctions; using RulesEngine.HelperFunctions;
@ -10,7 +9,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Runtime.InteropServices;
namespace RulesEngine namespace RulesEngine
{ {
@ -30,20 +28,13 @@ namespace RulesEngine
private readonly RuleExpressionBuilderFactory _expressionBuilderFactory; private readonly RuleExpressionBuilderFactory _expressionBuilderFactory;
private readonly ReSettings _reSettings; private readonly ReSettings _reSettings;
/// <summary>
/// The logger
/// </summary>
private readonly ILogger _logger;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RuleCompiler"/> class. /// Initializes a new instance of the <see cref="RuleCompiler"/> class.
/// </summary> /// </summary>
/// <param name="expressionBuilderFactory">The expression builder factory.</param> /// <param name="expressionBuilderFactory">The expression builder factory.</param>
/// <exception cref="ArgumentNullException">expressionBuilderFactory</exception> /// <exception cref="ArgumentNullException">expressionBuilderFactory</exception>
internal RuleCompiler(RuleExpressionBuilderFactory expressionBuilderFactory, ReSettings reSettings, ILogger logger) internal RuleCompiler(RuleExpressionBuilderFactory expressionBuilderFactory, ReSettings reSettings)
{ {
_logger = logger ?? throw new ArgumentNullException($"{nameof(logger)} can't be null.");
_expressionBuilderFactory = expressionBuilderFactory ?? throw new ArgumentNullException($"{nameof(expressionBuilderFactory)} can't be null."); _expressionBuilderFactory = expressionBuilderFactory ?? throw new ArgumentNullException($"{nameof(expressionBuilderFactory)} can't be null.");
_reSettings = reSettings; _reSettings = reSettings;
} }
@ -61,7 +52,6 @@ namespace RulesEngine
if (rule == null) if (rule == null)
{ {
var ex = new ArgumentNullException(nameof(rule)); var ex = new ArgumentNullException(nameof(rule));
_logger.LogError(ex.Message);
throw ex; throw ex;
} }
try try
@ -75,7 +65,6 @@ namespace RulesEngine
catch (Exception ex) catch (Exception ex)
{ {
var message = $"Error while compiling rule `{rule.RuleName}`: {ex.Message}"; var message = $"Error while compiling rule `{rule.RuleName}`: {ex.Message}";
_logger.LogError(message);
return Helpers.ToRuleExceptionResult(_reSettings, rule, new RuleException(message, ex)); return Helpers.ToRuleExceptionResult(_reSettings, rule, new RuleException(message, ex));
} }
} }
@ -131,7 +120,7 @@ namespace RulesEngine
{ {
try try
{ {
var lpExpression = expressionBuilder.Parse(lp.Expression, parameters.ToArray(), null).Body; var lpExpression = expressionBuilder.Parse(lp.Expression, parameters.ToArray(), null);
var ruleExpParam = new RuleExpressionParameter() { var ruleExpParam = new RuleExpressionParameter() {
ParameterExpression = Expression.Parameter(lpExpression.Type, lp.Name), ParameterExpression = Expression.Parameter(lpExpression.Type, lp.Name),
ValueExpression = lpExpression ValueExpression = lpExpression
@ -187,7 +176,7 @@ namespace RulesEngine
return (paramArray) => { return (paramArray) => {
var (isSuccess, resultList) = ApplyOperation(paramArray, ruleFuncList, operation); var (isSuccess, resultList) = ApplyOperation(paramArray, ruleFuncList, operation);
Func<object[], bool> isSuccessFn = (p) => isSuccess; bool isSuccessFn(object[] p) => isSuccess;
var result = Helpers.ToResultTree(_reSettings, parentRule, resultList, isSuccessFn); var result = Helpers.ToResultTree(_reSettings, parentRule, resultList, isSuccessFn);
return result(paramArray); return result(paramArray);
}; };

View File

@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. // Copyright (c) Microsoft Corporation.
// Licensed under the MIT License. // Licensed under the MIT License.
using RulesEngine.HelperFunctions;
using RulesEngine.Models; using RulesEngine.Models;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
@ -13,10 +14,17 @@ namespace RulesEngine
internal class RulesCache internal class RulesCache
{ {
/// <summary>The compile rules</summary> /// <summary>The compile rules</summary>
private ConcurrentDictionary<string, (IDictionary<string, RuleFunc<RuleResultTree>>, Int64)> _compileRules = new ConcurrentDictionary<string, (IDictionary<string, RuleFunc<RuleResultTree>>, Int64)>(); private readonly MemCache _compileRules;
/// <summary>The workflow rules</summary> /// <summary>The workflow rules</summary>
private ConcurrentDictionary<string, (Workflow, Int64)> _workflow = new ConcurrentDictionary<string, (Workflow, Int64)>(); private readonly ConcurrentDictionary<string, (Workflow, long)> _workflow = new ConcurrentDictionary<string, (Workflow, long)>();
public RulesCache(ReSettings reSettings)
{
_compileRules = new MemCache(reSettings.CacheConfig);
}
/// <summary>Determines whether [contains workflow rules] [the specified workflow name].</summary> /// <summary>Determines whether [contains workflow rules] [the specified workflow name].</summary>
/// <param name="workflowName">Name of the workflow.</param> /// <param name="workflowName">Name of the workflow.</param>
@ -32,21 +40,12 @@ namespace RulesEngine
return _workflow.Keys.ToList(); return _workflow.Keys.ToList();
} }
/// <summary>Determines whether [contains compiled rules] [the specified workflow name].</summary>
/// <param name="workflowName">Name of the workflow.</param>
/// <returns>
/// <c>true</c> if [contains compiled rules] [the specified workflow name]; otherwise, <c>false</c>.</returns>
public bool ContainsCompiledRules(string workflowName)
{
return _compileRules.ContainsKey(workflowName);
}
/// <summary>Adds the or update workflow rules.</summary> /// <summary>Adds the or update workflow rules.</summary>
/// <param name="workflowName">Name of the workflow.</param> /// <param name="workflowName">Name of the workflow.</param>
/// <param name="rules">The rules.</param> /// <param name="rules">The rules.</param>
public void AddOrUpdateWorkflows(string workflowName, Workflow rules) public void AddOrUpdateWorkflows(string workflowName, Workflow rules)
{ {
Int64 ticks = DateTime.UtcNow.Ticks; long ticks = DateTime.UtcNow.Ticks;
_workflow.AddOrUpdate(workflowName, (rules, ticks), (k, v) => (rules, ticks)); _workflow.AddOrUpdate(workflowName, (rules, ticks), (k, v) => (rules, ticks));
} }
@ -55,8 +54,8 @@ namespace RulesEngine
/// <param name="compiledRule">The compiled rule.</param> /// <param name="compiledRule">The compiled rule.</param>
public void AddOrUpdateCompiledRule(string compiledRuleKey, IDictionary<string, RuleFunc<RuleResultTree>> compiledRule) public void AddOrUpdateCompiledRule(string compiledRuleKey, IDictionary<string, RuleFunc<RuleResultTree>> compiledRule)
{ {
Int64 ticks = DateTime.UtcNow.Ticks; long ticks = DateTime.UtcNow.Ticks;
_compileRules.AddOrUpdate(compiledRuleKey, (compiledRule, ticks), (k, v) => (compiledRule, ticks)); _compileRules.Set(compiledRuleKey,(compiledRule, ticks));
} }
/// <summary>Checks if the compiled rules are up-to-date.</summary> /// <summary>Checks if the compiled rules are up-to-date.</summary>
@ -66,9 +65,9 @@ namespace RulesEngine
/// <c>true</c> if [compiled rules] is newer than the [workflow rules]; otherwise, <c>false</c>.</returns> /// <c>true</c> if [compiled rules] is newer than the [workflow rules]; otherwise, <c>false</c>.</returns>
public bool AreCompiledRulesUpToDate(string compiledRuleKey, string workflowName) public bool AreCompiledRulesUpToDate(string compiledRuleKey, string workflowName)
{ {
if (_compileRules.TryGetValue(compiledRuleKey, out (IDictionary<string, RuleFunc<RuleResultTree>> rules, Int64 tick) compiledRulesObj)) if (_compileRules.TryGetValue(compiledRuleKey, out (IDictionary<string, RuleFunc<RuleResultTree>> rules, long tick) compiledRulesObj))
{ {
if (_workflow.TryGetValue(workflowName, out (Workflow rules, Int64 tick) WorkflowsObj)) if (_workflow.TryGetValue(workflowName, out (Workflow rules, long tick) WorkflowsObj))
{ {
return compiledRulesObj.tick >= WorkflowsObj.tick; return compiledRulesObj.tick >= WorkflowsObj.tick;
} }
@ -90,7 +89,7 @@ namespace RulesEngine
/// <exception cref="Exception">Could not find injected Workflow: {wfname}</exception> /// <exception cref="Exception">Could not find injected Workflow: {wfname}</exception>
public Workflow GetWorkflow(string workflowName) public Workflow GetWorkflow(string workflowName)
{ {
if (_workflow.TryGetValue(workflowName, out (Workflow rules, Int64 tick) WorkflowsObj)) if (_workflow.TryGetValue(workflowName, out (Workflow rules, long tick) WorkflowsObj))
{ {
var workflow = WorkflowsObj.rules; var workflow = WorkflowsObj.rules;
if (workflow.WorkflowsToInject?.Any() == true) if (workflow.WorkflowsToInject?.Any() == true)
@ -125,19 +124,19 @@ namespace RulesEngine
/// <returns>CompiledRule.</returns> /// <returns>CompiledRule.</returns>
public IDictionary<string, RuleFunc<RuleResultTree>> GetCompiledRules(string compiledRulesKey) public IDictionary<string, RuleFunc<RuleResultTree>> GetCompiledRules(string compiledRulesKey)
{ {
return _compileRules[compiledRulesKey].Item1; return _compileRules.Get<(IDictionary<string, RuleFunc<RuleResultTree>> rules, long tick)>(compiledRulesKey).rules;
} }
/// <summary>Removes the specified workflow name.</summary> /// <summary>Removes the specified workflow name.</summary>
/// <param name="workflowName">Name of the workflow.</param> /// <param name="workflowName">Name of the workflow.</param>
public void Remove(string workflowName) public void Remove(string workflowName)
{ {
if (_workflow.TryRemove(workflowName, out (Workflow, Int64) workflowObj)) if (_workflow.TryRemove(workflowName, out var workflowObj))
{ {
var compiledKeysToRemove = _compileRules.Keys.Where(key => key.StartsWith(workflowName)); var compiledKeysToRemove = _compileRules.GetKeys().Where(key => key.StartsWith(workflowName));
foreach (var key in compiledKeysToRemove) foreach (var key in compiledKeysToRemove)
{ {
_compileRules.TryRemove(key, out (IDictionary<string, RuleFunc<RuleResultTree>>, Int64) val); _compileRules.Remove(key);
} }
} }
} }

View File

@ -1,431 +1,427 @@
// Copyright (c) Microsoft Corporation. // Copyright (c) Microsoft Corporation.
// Licensed under the MIT License. // Licensed under the MIT License.
using FluentValidation; using FluentValidation;
using Microsoft.Extensions.Logging; using Newtonsoft.Json;
using Microsoft.Extensions.Logging.Abstractions; using Newtonsoft.Json.Linq;
using Newtonsoft.Json; using RulesEngine.Actions;
using Newtonsoft.Json.Linq; using RulesEngine.Exceptions;
using RulesEngine.Actions; using RulesEngine.ExpressionBuilders;
using RulesEngine.Exceptions; using RulesEngine.HelperFunctions;
using RulesEngine.ExpressionBuilders; using RulesEngine.Interfaces;
using RulesEngine.Interfaces; using RulesEngine.Models;
using RulesEngine.Models; using RulesEngine.Validators;
using RulesEngine.Validators; using System;
using System; using System.Collections.Generic;
using System.Collections.Generic; using System.Linq;
using System.Linq; using System.Text.RegularExpressions;
using System.Text.RegularExpressions; using System.Threading.Tasks;
using System.Threading.Tasks;
namespace RulesEngine
namespace RulesEngine {
{ /// <summary>
/// <summary> ///
/// /// </summary>
/// </summary> /// <seealso cref="RulesEngine.Interfaces.IRulesEngine" />
/// <seealso cref="RulesEngine.Interfaces.IRulesEngine" /> public class RulesEngine : IRulesEngine
public class RulesEngine : IRulesEngine {
{ #region Variables
#region Variables private readonly ReSettings _reSettings;
private readonly ILogger _logger; private readonly RulesCache _rulesCache;
private readonly ReSettings _reSettings; private readonly RuleExpressionParser _ruleExpressionParser;
private readonly RulesCache _rulesCache = new RulesCache(); private readonly RuleCompiler _ruleCompiler;
private readonly RuleExpressionParser _ruleExpressionParser; private readonly ActionFactory _actionFactory;
private readonly RuleCompiler _ruleCompiler; private const string ParamParseRegex = "(\\$\\(.*?\\))";
private readonly ActionFactory _actionFactory; #endregion
private const string ParamParseRegex = "(\\$\\(.*?\\))";
#endregion #region Constructor
public RulesEngine(string[] jsonConfig, ReSettings reSettings = null) : this(reSettings)
#region Constructor {
public RulesEngine(string[] jsonConfig, ILogger logger = null, ReSettings reSettings = null) : this(logger, reSettings) var workflow = jsonConfig.Select(item => JsonConvert.DeserializeObject<Workflow>(item)).ToArray();
{ AddWorkflow(workflow);
var workflow = jsonConfig.Select(item => JsonConvert.DeserializeObject<Workflow>(item)).ToArray();
AddWorkflow(workflow);
}
public RulesEngine(Workflow[] Workflows, ILogger logger = null, ReSettings reSettings = null) : this(logger, reSettings)
{
AddWorkflow(Workflows);
}
public RulesEngine(ILogger logger = null, ReSettings reSettings = null)
{
_logger = logger ?? new NullLogger<RulesEngine>();
_reSettings = reSettings ?? new ReSettings();
_ruleExpressionParser = new RuleExpressionParser(_reSettings);
_ruleCompiler = new RuleCompiler(new RuleExpressionBuilderFactory(_reSettings, _ruleExpressionParser),_reSettings, _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 (var i = 0; i < inputs.Length; i++)
{
var input = inputs[i];
ruleParams.Add(new RuleParameter($"input{i + 1}", input));
}
return await ExecuteAllRulesAsync(workflowName, ruleParams.ToArray());
}
/// <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);
await ExecuteActionAsync(ruleResultList);
return ruleResultList;
}
private async ValueTask ExecuteActionAsync(IEnumerable<RuleResultTree> ruleResultList)
{
foreach (var ruleResult in ruleResultList)
{
if(ruleResult.ChildResults != null)
{
await ExecuteActionAsync(ruleResult.ChildResults);
}
var actionResult = await ExecuteActionForRuleResult(ruleResult, false);
ruleResult.ActionResult = new ActionResult {
Output = actionResult.Output,
Exception = actionResult.Exception
};
}
}
public async ValueTask<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)
{
var ruleActions = resultTree?.Rule?.Actions;
var actionInfo = resultTree?.IsSuccess == true ? ruleActions?.OnSuccess : ruleActions?.OnFailure;
if (actionInfo != null)
{
var action = _actionFactory.Get(actionInfo.Name);
var ruleParameters = resultTree.Inputs.Select(kv => new RuleParameter(kv.Key, kv.Value)).ToArray();
return await action.ExecuteAndReturnResultAsync(new ActionContext(actionInfo.Context, resultTree), ruleParameters, includeRuleResults);
}
else
{
//If there is no action,return output as null and return the result for rule
return new ActionRuleResult {
Output = null,
Results = includeRuleResults ? new List<RuleResultTree>() { resultTree } : null
};
}
}
#endregion
#region Private Methods
/// <summary>
/// Adds the workflow if the workflow name is not already added. Ignores the rest.
/// </summary>
/// <param name="workflows">The workflow rules.</param>
/// <exception cref="RuleValidationException"></exception>
public void AddWorkflow(params Workflow[] workflows)
{
try
{
foreach (var workflow in workflows)
{
var validator = new WorkflowsValidator();
validator.ValidateAndThrow(workflow);
if (!_rulesCache.ContainsWorkflows(workflow.WorkflowName))
{
_rulesCache.AddOrUpdateWorkflows(workflow.WorkflowName, workflow);
}
else
{
throw new ValidationException($"Cannot add workflow `{workflow.WorkflowName}` as it already exists. Use `AddOrUpdateWorkflow` to update existing workflow");
}
}
}
catch (ValidationException ex)
{
throw new RuleValidationException(ex.Message, ex.Errors);
}
}
/// <summary>
/// Adds new workflow rules if not previously added.
/// Or updates the rules for an existing workflow.
/// </summary>
/// <param name="workflows">The workflow rules.</param>
/// <exception cref="RuleValidationException"></exception>
public void AddOrUpdateWorkflow(params Workflow[] workflows)
{
try
{
foreach (var workflow in workflows)
{
var validator = new WorkflowsValidator();
validator.ValidateAndThrow(workflow);
_rulesCache.AddOrUpdateWorkflows(workflow.WorkflowName, workflow);
}
}
catch (ValidationException ex)
{
throw new RuleValidationException(ex.Message, ex.Errors);
}
}
public List<string> GetAllRegisteredWorkflowNames()
{
return _rulesCache.GetAllWorkflowNames();
} }
/// <summary> public RulesEngine(Workflow[] Workflows, ReSettings reSettings = null) : this(reSettings)
/// Checks is workflow exist. {
/// </summary> AddWorkflow(Workflows);
/// <param name="workflowName">The workflow name.</param> }
/// <returns> <c>true</c> if contains the specified workflow name; otherwise, <c>false</c>.</returns>
public bool ContainsWorkflow(string workflowName) public RulesEngine(ReSettings reSettings = null)
{ {
return _rulesCache.ContainsWorkflows(workflowName); _reSettings = reSettings ?? new ReSettings();
} if(_reSettings.CacheConfig == null)
{
/// <summary> _reSettings.CacheConfig = new MemCacheConfig();
/// Clears the workflow. }
/// </summary> _rulesCache = new RulesCache(_reSettings);
public void ClearWorkflows() _ruleExpressionParser = new RuleExpressionParser(_reSettings);
{ _ruleCompiler = new RuleCompiler(new RuleExpressionBuilderFactory(_reSettings, _ruleExpressionParser),_reSettings);
_rulesCache.Clear(); _actionFactory = new ActionFactory(GetActionRegistry(_reSettings));
} }
/// <summary> private IDictionary<string, Func<ActionBase>> GetActionRegistry(ReSettings reSettings)
/// Removes the workflows. {
/// </summary> var actionDictionary = GetDefaultActionRegistry();
/// <param name="workflowNames">The workflow names.</param> var customActions = reSettings.CustomActions ?? new Dictionary<string, Func<ActionBase>>();
public void RemoveWorkflow(params string[] workflowNames) foreach (var customAction in customActions)
{ {
foreach (var workflowName in workflowNames) actionDictionary.Add(customAction);
{ }
_rulesCache.Remove(workflowName); return actionDictionary;
}
} }
#endregion
/// <summary>
/// This will validate workflow rules then call execute method #region Public Methods
/// </summary>
/// <typeparam name="T">type of entity</typeparam> /// <summary>
/// <param name="input">input</param> /// This will execute all the rules of the specified workflow
/// <param name="workflowName">workflow name</param> /// </summary>
/// <returns>list of rule result set</returns> /// <param name="workflowName">The name of the workflow with rules to execute against the inputs</param>
private List<RuleResultTree> ValidateWorkflowAndExecuteRule(string workflowName, RuleParameter[] ruleParams) /// <param name="inputs">A variable number of inputs</param>
{ /// <returns>List of rule results</returns>
List<RuleResultTree> result; public async ValueTask<List<RuleResultTree>> ExecuteAllRulesAsync(string workflowName, params object[] inputs)
{
if (RegisterRule(workflowName, ruleParams)) var ruleParams = new List<RuleParameter>();
{
result = ExecuteAllRuleByWorkflow(workflowName, ruleParams); for (var i = 0; i < inputs.Length; i++)
} {
else var input = inputs[i];
{ ruleParams.Add(new RuleParameter($"input{i + 1}", input));
_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 await ExecuteAllRulesAsync(workflowName, ruleParams.ToArray());
} }
return result;
} /// <summary>
/// This will execute all the rules of the specified workflow
/// <summary> /// </summary>
/// This will compile the rules and store them to dictionary /// <param name="workflowName">The name of the workflow with rules to execute against the inputs</param>
/// </summary> /// <param name="ruleParams">A variable number of rule parameters</param>
/// <param name="workflowName">workflow name</param> /// <returns>List of rule results</returns>
/// <param name="ruleParams">The rule parameters.</param> public async ValueTask<List<RuleResultTree>> ExecuteAllRulesAsync(string workflowName, params RuleParameter[] ruleParams)
/// <returns> {
/// bool result var ruleResultList = ValidateWorkflowAndExecuteRule(workflowName, ruleParams);
/// </returns> await ExecuteActionAsync(ruleResultList);
private bool RegisterRule(string workflowName, params RuleParameter[] ruleParams) return ruleResultList;
{ }
var compileRulesKey = GetCompiledRulesKey(workflowName, ruleParams);
if (_rulesCache.AreCompiledRulesUpToDate(compileRulesKey, workflowName)) private async ValueTask ExecuteActionAsync(IEnumerable<RuleResultTree> ruleResultList)
{ {
return true; foreach (var ruleResult in ruleResultList)
} {
if(ruleResult.ChildResults != null)
var workflow = _rulesCache.GetWorkflow(workflowName); {
if (workflow != null) await ExecuteActionAsync(ruleResult.ChildResults);
{ }
var dictFunc = new Dictionary<string, RuleFunc<RuleResultTree>>(); var actionResult = await ExecuteActionForRuleResult(ruleResult, false);
foreach (var rule in workflow.Rules.Where(c => c.Enabled)) ruleResult.ActionResult = new ActionResult {
{ Output = actionResult.Output,
dictFunc.Add(rule.RuleName, CompileRule(rule, ruleParams, workflow.GlobalParams?.ToArray())); Exception = actionResult.Exception
} };
}
_rulesCache.AddOrUpdateCompiledRule(compileRulesKey, dictFunc); }
_logger.LogTrace($"Rules has been compiled for the {workflowName} workflow and added to dictionary");
return true; public async ValueTask<ActionRuleResult> ExecuteActionWorkflowAsync(string workflowName, string ruleName, RuleParameter[] ruleParameters)
} {
else var compiledRule = CompileRule(workflowName, ruleName, ruleParameters);
{ var resultTree = compiledRule(ruleParameters);
return false; return await ExecuteActionForRuleResult(resultTree, true);
} }
}
private async ValueTask<ActionRuleResult> ExecuteActionForRuleResult(RuleResultTree resultTree, bool includeRuleResults = false)
{
private RuleFunc<RuleResultTree> CompileRule(string workflowName, string ruleName, RuleParameter[] ruleParameters) var ruleActions = resultTree?.Rule?.Actions;
{ var actionInfo = resultTree?.IsSuccess == true ? ruleActions?.OnSuccess : ruleActions?.OnFailure;
var workflow = _rulesCache.GetWorkflow(workflowName);
if(workflow == null) if (actionInfo != null)
{ {
throw new ArgumentException($"Workflow `{workflowName}` is not found"); var action = _actionFactory.Get(actionInfo.Name);
} var ruleParameters = resultTree.Inputs.Select(kv => new RuleParameter(kv.Key, kv.Value)).ToArray();
var currentRule = workflow.Rules?.SingleOrDefault(c => c.RuleName == ruleName && c.Enabled); return await action.ExecuteAndReturnResultAsync(new ActionContext(actionInfo.Context, resultTree), ruleParameters, includeRuleResults);
if (currentRule == null) }
{ else
throw new ArgumentException($"Workflow `{workflowName}` does not contain any rule named `{ruleName}`"); {
} //If there is no action,return output as null and return the result for rule
return CompileRule(currentRule, ruleParameters, workflow.GlobalParams?.ToArray()); return new ActionRuleResult {
} Output = null,
Results = includeRuleResults ? new List<RuleResultTree>() { resultTree } : null
private RuleFunc<RuleResultTree> CompileRule(Rule rule, RuleParameter[] ruleParams, ScopedParam[] scopedParams) };
{ }
return _ruleCompiler.CompileRule(rule, ruleParams, scopedParams); }
}
#endregion
#region Private Methods
/// <summary>
/// This will execute the compiled rules /// <summary>
/// </summary> /// Adds the workflow if the workflow name is not already added. Ignores the rest.
/// <param name="workflowName"></param> /// </summary>
/// <param name="ruleParams"></param> /// <param name="workflows">The workflow rules.</param>
/// <returns>list of rule result set</returns> /// <exception cref="RuleValidationException"></exception>
private List<RuleResultTree> ExecuteAllRuleByWorkflow(string workflowName, RuleParameter[] ruleParameters) public void AddWorkflow(params Workflow[] workflows)
{ {
_logger.LogTrace($"Compiled rules found for {workflowName} workflow and executed"); try
{
var result = new List<RuleResultTree>(); foreach (var workflow in workflows)
var compiledRulesCacheKey = GetCompiledRulesKey(workflowName, ruleParameters); {
foreach (var compiledRule in _rulesCache.GetCompiledRules(compiledRulesCacheKey)?.Values) var validator = new WorkflowsValidator();
{ validator.ValidateAndThrow(workflow);
var resultTree = compiledRule(ruleParameters); if (!_rulesCache.ContainsWorkflows(workflow.WorkflowName))
result.Add(resultTree); {
} _rulesCache.AddOrUpdateWorkflows(workflow.WorkflowName, workflow);
}
FormatErrorMessages(result); else
return result; {
} throw new ValidationException($"Cannot add workflow `{workflow.WorkflowName}` as it already exists. Use `AddOrUpdateWorkflow` to update existing workflow");
}
private string GetCompiledRulesKey(string workflowName, RuleParameter[] ruleParams) }
{ }
var key = $"{workflowName}-" + string.Join("-", ruleParams.Select(c => c.Type.Name)); catch (ValidationException ex)
return key; {
} throw new RuleValidationException(ex.Message, ex.Errors);
}
private IDictionary<string, Func<ActionBase>> GetDefaultActionRegistry() }
{
return new Dictionary<string, Func<ActionBase>>{ /// <summary>
{"OutputExpression",() => new OutputExpressionAction(_ruleExpressionParser) }, /// Adds new workflow rules if not previously added.
{"EvaluateRule", () => new EvaluateRuleAction(this,_ruleExpressionParser) } /// Or updates the rules for an existing workflow.
}; /// </summary>
} /// <param name="workflows">The workflow rules.</param>
/// <exception cref="RuleValidationException"></exception>
/// <summary> public void AddOrUpdateWorkflow(params Workflow[] workflows)
/// The result {
/// </summary> try
/// <param name="ruleResultList">The result.</param> {
/// <returns>Updated error message.</returns> foreach (var workflow in workflows)
private IEnumerable<RuleResultTree> FormatErrorMessages(IEnumerable<RuleResultTree> ruleResultList) {
{ var validator = new WorkflowsValidator();
if (_reSettings.EnableFormattedErrorMessage) validator.ValidateAndThrow(workflow);
{ _rulesCache.AddOrUpdateWorkflows(workflow.WorkflowName, workflow);
foreach (var ruleResult in ruleResultList?.Where(r => !r.IsSuccess)) }
{ }
var errorMessage = ruleResult?.Rule?.ErrorMessage; catch (ValidationException ex)
if (string.IsNullOrWhiteSpace(ruleResult.ExceptionMessage) && errorMessage != null) {
{ throw new RuleValidationException(ex.Message, ex.Errors);
var errorParameters = Regex.Matches(errorMessage, ParamParseRegex); }
}
var inputs = ruleResult.Inputs;
foreach (var param in errorParameters) public List<string> GetAllRegisteredWorkflowNames()
{ {
var paramVal = param?.ToString(); return _rulesCache.GetAllWorkflowNames();
var property = paramVal?.Substring(2, paramVal.Length - 3); }
if (property?.Split('.')?.Count() > 1)
{ /// <summary>
var typeName = property?.Split('.')?[0]; /// Checks is workflow exist.
var propertyName = property?.Split('.')?[1]; /// </summary>
errorMessage = UpdateErrorMessage(errorMessage, inputs, property, typeName, propertyName); /// <param name="workflowName">The workflow name.</param>
} /// <returns> <c>true</c> if contains the specified workflow name; otherwise, <c>false</c>.</returns>
else public bool ContainsWorkflow(string workflowName)
{ {
var arrParams = inputs?.Select(c => new { Name = c.Key, c.Value }); return _rulesCache.ContainsWorkflows(workflowName);
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})"); /// <summary>
} /// Clears the workflow.
} /// </summary>
ruleResult.ExceptionMessage = errorMessage; public void ClearWorkflows()
} {
_rulesCache.Clear();
} }
}
return ruleResultList; /// <summary>
} /// Removes the workflows.
/// </summary>
/// <summary> /// <param name="workflowNames">The workflow names.</param>
/// Updates the error message. public void RemoveWorkflow(params string[] workflowNames)
/// </summary> {
/// <param name="errorMessage">The error message.</param> foreach (var workflowName in workflowNames)
/// <param name="evaluatedParams">The evaluated parameters.</param> {
/// <param name="property">The property.</param> _rulesCache.Remove(workflowName);
/// <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) /// <summary>
{ /// This will validate workflow rules then call execute method
var arrParams = inputs?.Select(c => new { Name = c.Key, c.Value }); /// </summary>
var model = arrParams?.Where(a => string.Equals(a.Name, typeName))?.FirstOrDefault(); /// <typeparam name="T">type of entity</typeparam>
if (model != null) /// <param name="input">input</param>
{ /// <param name="workflowName">workflow name</param>
var modelJson = JsonConvert.SerializeObject(model?.Value); /// <returns>list of rule result set</returns>
var jObj = JObject.Parse(modelJson); private List<RuleResultTree> ValidateWorkflowAndExecuteRule(string workflowName, RuleParameter[] ruleParams)
JToken jToken = null; {
var val = jObj?.TryGetValue(propertyName, StringComparison.OrdinalIgnoreCase, out jToken); List<RuleResultTree> result;
errorMessage = errorMessage.Replace($"$({property})", jToken != null ? jToken?.ToString() : $"({property})");
} if (RegisterRule(workflowName, ruleParams))
{
return errorMessage; result = ExecuteAllRuleByWorkflow(workflowName, ruleParams);
} }
#endregion else
} {
} // 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)
{
var compileRulesKey = GetCompiledRulesKey(workflowName, ruleParams);
if (_rulesCache.AreCompiledRulesUpToDate(compileRulesKey, workflowName))
{
return true;
}
var workflow = _rulesCache.GetWorkflow(workflowName);
if (workflow != null)
{
var dictFunc = new Dictionary<string, RuleFunc<RuleResultTree>>();
foreach (var rule in workflow.Rules.Where(c => c.Enabled))
{
dictFunc.Add(rule.RuleName, CompileRule(rule, ruleParams, workflow.GlobalParams?.ToArray()));
}
_rulesCache.AddOrUpdateCompiledRule(compileRulesKey, dictFunc);
return true;
}
else
{
return false;
}
}
private RuleFunc<RuleResultTree> CompileRule(string workflowName, string ruleName, RuleParameter[] ruleParameters)
{
var workflow = _rulesCache.GetWorkflow(workflowName);
if(workflow == null)
{
throw new ArgumentException($"Workflow `{workflowName}` is not found");
}
var currentRule = workflow.Rules?.SingleOrDefault(c => c.RuleName == ruleName && c.Enabled);
if (currentRule == null)
{
throw new ArgumentException($"Workflow `{workflowName}` does not contain any rule named `{ruleName}`");
}
return CompileRule(currentRule, ruleParameters, workflow.GlobalParams?.ToArray());
}
private RuleFunc<RuleResultTree> CompileRule(Rule rule, RuleParameter[] ruleParams, ScopedParam[] scopedParams)
{
return _ruleCompiler.CompileRule(rule, ruleParams, scopedParams);
}
/// <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)
{
var result = new List<RuleResultTree>();
var compiledRulesCacheKey = GetCompiledRulesKey(workflowName, ruleParameters);
foreach (var compiledRule in _rulesCache.GetCompiledRules(compiledRulesCacheKey)?.Values)
{
var resultTree = compiledRule(ruleParameters);
result.Add(resultTree);
}
FormatErrorMessages(result);
return result;
}
private string GetCompiledRulesKey(string workflowName, RuleParameter[] ruleParams)
{
var key = $"{workflowName}-" + string.Join("-", ruleParams.Select(c => c.Type.Name));
return key;
}
private IDictionary<string, Func<ActionBase>> GetDefaultActionRegistry()
{
return new Dictionary<string, Func<ActionBase>>{
{"OutputExpression",() => new OutputExpressionAction(_ruleExpressionParser) },
{"EvaluateRule", () => new EvaluateRuleAction(this,_ruleExpressionParser) }
};
}
/// <summary>
/// The result
/// </summary>
/// <param name="ruleResultList">The result.</param>
/// <returns>Updated error message.</returns>
private IEnumerable<RuleResultTree> FormatErrorMessages(IEnumerable<RuleResultTree> ruleResultList)
{
if (_reSettings.EnableFormattedErrorMessage)
{
foreach (var ruleResult in ruleResultList?.Where(r => !r.IsSuccess))
{
var errorMessage = ruleResult?.Rule?.ErrorMessage;
if (string.IsNullOrWhiteSpace(ruleResult.ExceptionMessage) && errorMessage != null)
{
var errorParameters = Regex.Matches(errorMessage, ParamParseRegex);
var inputs = ruleResult.Inputs;
foreach (var param in errorParameters)
{
var paramVal = param?.ToString();
var property = paramVal?.Substring(2, paramVal.Length - 3);
if (property?.Split('.')?.Count() > 1)
{
var typeName = property?.Split('.')?[0];
var propertyName = property?.Split('.')?[1];
errorMessage = UpdateErrorMessage(errorMessage, inputs, property, typeName, propertyName);
}
else
{
var arrParams = inputs?.Select(c => new { Name = c.Key, c.Value });
var model = arrParams?.Where(a => string.Equals(a.Name, property))?.FirstOrDefault();
var value = model?.Value != null ? JsonConvert.SerializeObject(model?.Value) : null;
errorMessage = errorMessage?.Replace($"$({property})", value ?? $"$({property})");
}
}
ruleResult.ExceptionMessage = errorMessage;
}
}
}
return ruleResultList;
}
/// <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

@ -31,15 +31,13 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FastExpressionCompiler" Version="3.2.1" /> <PackageReference Include="FastExpressionCompiler" Version="3.2.2" />
<PackageReference Include="FluentValidation" Version="10.3.0" /> <PackageReference Include="FluentValidation" Version="10.4.0" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" /> <PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="System.Linq" Version="4.3.0" /> <PackageReference Include="System.Linq" Version="4.3.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.2.14" /> <PackageReference Include="System.Linq.Dynamic.Core" Version="1.2.18" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" /> <PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
</ItemGroup> </ItemGroup>

View File

@ -19,7 +19,7 @@ namespace RulesEngine.UnitTest.ActionTests
public async Task CustomActionOnRuleMustHaveContextValues() public async Task CustomActionOnRuleMustHaveContextValues()
{ {
var workflow = GetWorkflow(); var workflow = GetWorkflow();
var re = new RulesEngine(workflow, null, reSettings: new ReSettings { var re = new RulesEngine(workflow, reSettings: new ReSettings {
CustomActions = new Dictionary<string, System.Func<Actions.ActionBase>> { CustomActions = new Dictionary<string, System.Func<Actions.ActionBase>> {
{ "ReturnContext", () => new ReturnContextAction() } { "ReturnContext", () => new ReturnContextAction() }
@ -39,7 +39,7 @@ namespace RulesEngine.UnitTest.ActionTests
var workflowViaTextJson = System.Text.Json.JsonSerializer.Deserialize<Workflow[]>(workflowStr,serializationOptions); var workflowViaTextJson = System.Text.Json.JsonSerializer.Deserialize<Workflow[]>(workflowStr,serializationOptions);
var re = new RulesEngine(workflow, null, reSettings: new ReSettings { var re = new RulesEngine(workflow, reSettings: new ReSettings {
CustomActions = new Dictionary<string, System.Func<Actions.ActionBase>> { CustomActions = new Dictionary<string, System.Func<Actions.ActionBase>> {
{ "ReturnContext", () => new ReturnContextAction() } { "ReturnContext", () => new ReturnContextAction() }

View File

@ -23,6 +23,16 @@ namespace RulesEngine.UnitTest
Assert.Equal(2 * 2, result.Output); Assert.Equal(2 * 2, result.Output);
} }
[Fact]
public async Task WhenExpressionIsSuccess_ComplexOutputExpressionAction_ReturnsExpressionEvaluation()
{
var engine = new RulesEngine(GetWorkflowWithActions());
var result = await engine.ExecuteActionWorkflowAsync("ActionWorkflow", "ComplexOutputRuleTest", new RuleParameter[0]);
Assert.NotNull(result);
dynamic output = result.Output;
Assert.Equal(2, output.test);
}
[Fact] [Fact]
public async Task WhenExpressionIsSuccess_EvaluateRuleAction_ReturnsExpressionEvaluation() public async Task WhenExpressionIsSuccess_EvaluateRuleAction_ReturnsExpressionEvaluation()
{ {
@ -108,6 +118,19 @@ namespace RulesEngine.UnitTest
} }
} }
}, },
new Rule{
RuleName = "ComplexOutputRuleTest",
RuleExpressionType = RuleExpressionType.LambdaExpression,
Expression = "1 == 1",
Actions = new RuleActions{
OnSuccess = new ActionInfo{
Name = "OutputExpression",
Context = new Dictionary<string, object>{
{"expression", "new (2 as test)"}
}
}
}
},
new Rule{ new Rule{
RuleName = "EvaluateRuleTest", RuleName = "EvaluateRuleTest",
RuleExpressionType = RuleExpressionType.LambdaExpression, RuleExpressionType = RuleExpressionType.LambdaExpression,

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation. // Copyright (c) Microsoft Corporation.
// Licensed under the MIT License. // Licensed under the MIT License.
using Microsoft.Extensions.Logging.Abstractions;
using RulesEngine.ExpressionBuilders; using RulesEngine.ExpressionBuilders;
using RulesEngine.Models; using RulesEngine.Models;
using System; using System;
@ -17,10 +16,10 @@ namespace RulesEngine.UnitTest
[Fact] [Fact]
public void RuleCompiler_NullCheck() public void RuleCompiler_NullCheck()
{ {
Assert.Throws<ArgumentNullException>(() => new RuleCompiler(null, null,null)); Assert.Throws<ArgumentNullException>(() => new RuleCompiler(null, null));
var reSettings = new ReSettings(); var reSettings = new ReSettings();
var parser = new RuleExpressionParser(reSettings); var parser = new RuleExpressionParser(reSettings);
Assert.Throws<ArgumentNullException>(() => new RuleCompiler(new RuleExpressionBuilderFactory(reSettings, parser), null,null)); Assert.Throws<ArgumentNullException>(() => new RuleCompiler(null, null));
} }
[Fact] [Fact]
@ -28,11 +27,9 @@ namespace RulesEngine.UnitTest
{ {
var reSettings = new ReSettings(); var reSettings = new ReSettings();
var parser = new RuleExpressionParser(reSettings); var parser = new RuleExpressionParser(reSettings);
var compiler = new RuleCompiler(new RuleExpressionBuilderFactory(reSettings, parser),null, new NullLogger<RuleCompiler>()); var compiler = new RuleCompiler(new RuleExpressionBuilderFactory(reSettings, parser),null);
Assert.Throws<ArgumentNullException>(() => compiler.CompileRule(null, null,null)); Assert.Throws<ArgumentNullException>(() => compiler.CompileRule(null, null,null));
Assert.Throws<ArgumentNullException>(() => compiler.CompileRule(null, new RuleParameter[] { null },null)); Assert.Throws<ArgumentNullException>(() => compiler.CompileRule(null, new RuleParameter[] { null },null));
} }
} }
} }

View File

@ -1,21 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<SignAssembly>True</SignAssembly> <SignAssembly>True</SignAssembly>
<AssemblyOriginatorKeyFile>..\..\signing\RulesEngine-publicKey.snk</AssemblyOriginatorKeyFile> <AssemblyOriginatorKeyFile>..\..\signing\RulesEngine-publicKey.snk</AssemblyOriginatorKeyFile>
<DelaySign>True</DelaySign> <DelaySign>True</DelaySign>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoFixture" Version="4.17.0" /> <PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="System.Text.Json" Version="5.0.2" /> <PackageReference Include="System.Text.Json" Version="6.0.2" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.0"> <PackageReference Include="coverlet.collector" Version="3.1.2">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@ -48,6 +48,9 @@
<None Update="TestData\rules8.json"> <None Update="TestData\rules8.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Update="TestData\rules10.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestData\rules9.json"> <None Update="TestData\rules9.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>

View File

@ -26,7 +26,7 @@ namespace RulesEngine.UnitTest
{ {
var workflow = GetWorkflowList(); var workflow = GetWorkflowList();
var engine = new RulesEngine(null, null); var engine = new RulesEngine();
engine.AddWorkflow(workflow); engine.AddWorkflow(workflow);
var input1 = new { var input1 = new {
@ -47,7 +47,7 @@ namespace RulesEngine.UnitTest
{ {
var workflow = GetWorkflowList(); var workflow = GetWorkflowList();
var engine = new RulesEngine(null, null); var engine = new RulesEngine();
engine.AddWorkflow(workflow); engine.AddWorkflow(workflow);
var input1 = new { var input1 = new {
@ -77,7 +77,7 @@ namespace RulesEngine.UnitTest
{ {
var workflow = GetWorkflowList(); var workflow = GetWorkflowList();
var engine = new RulesEngine(new string[] { }, null, new ReSettings { var engine = new RulesEngine(new string[] { }, new ReSettings {
EnableScopedParams = false EnableScopedParams = false
}); });
engine.AddWorkflow(workflow); engine.AddWorkflow(workflow);

View File

@ -0,0 +1,11 @@
{
"WorkflowName": "inputWorkflow",
"Rules": [
{
"RuleName": "GiveDiscount10",
"SuccessEvent": "10",
"RuleExpressionType": "LambdaExpression",
"Expression": "input1.Data.GetProperty(\"category\").GetString() == \"abc\""
}
]
}

View File

@ -2,12 +2,20 @@
"WorkflowName": "inputWorkflow", "WorkflowName": "inputWorkflow",
"Rules": [ "Rules": [
{ {
"RuleName": "GiveDiscount10", "RuleName": "upperCaseAccess",
"SuccessEvent": "10", "SuccessEvent": "10",
"ErrorMessage": "One or more adjust rules failed.", "ErrorMessage": "One or more adjust rules failed.",
"ErrorType": "Error", "ErrorType": "Error",
"RuleExpressionType": "LambdaExpression", "RuleExpressionType": "LambdaExpression",
"Expression": "utils.CheckExists(String(input1.Property1)) == true" "Expression": "utils.CheckExists(String(input1.Property1)) == true"
},
{
"RuleName": "lowerCaseAccess",
"SuccessEvent": "10",
"ErrorMessage": "One or more adjust rules failed.",
"ErrorType": "Error",
"RuleExpressionType": "LambdaExpression",
"Expression": "utils.CheckExists(String(input1.property1)) == true"
} }
] ]
} }