Compare commits
87 Commits
v3.1.0-pre
...
main
Author | SHA1 | Date |
---|---|---|
dependabot[bot] | dc52989895 | |
dependabot[bot] | be6faef9f0 | |
Abbas Cyclewala | 9b53a9aa3a | |
Abbas Cyclewala | 22353a38d0 | |
Abbas Cyclewala | 9bcf4f334b | |
Abbas Cyclewala | b783602fe6 | |
Ayhan Doslu | 178248b5a9 | |
Abbas Cyclewala | 9e7918dde2 | |
dependabot[bot] | b9e1ece812 | |
dependabot[bot] | 00a73138b3 | |
Abbas Cyclewala | 103e817431 | |
dependabot[bot] | fe70cdad88 | |
Abbas Cyclewala | 47e7809591 | |
Abbas Cyclewala | caf41e3cd3 | |
dependabot[bot] | 7b4e45d2ad | |
dependabot[bot] | ef72cd0f60 | |
dependabot[bot] | a2f44b8806 | |
XiaoFei Du | a30247931d | |
dependabot[bot] | 7949a8b31d | |
Abbas Cyclewala | c5801e6ac6 | |
Abbas Cyclewala | f785c15f03 | |
Abbas Cyclewala | a76601b412 | |
Abbas Cyclewala | 48e22a03cf | |
Abbas Cyclewala | d0133155d6 | |
Steven Frew | a74f73a44e | |
Alex Reich | 77750ae0b4 | |
Kulshekhar Kabra | a0c424e742 | |
dependabot[bot] | dd9f6f3979 | |
dependabot[bot] | 840bcbc92b | |
microsoft-github-policy-service[bot] | ad46582151 | |
dependabot[bot] | 1503221cd4 | |
dependabot[bot] | 9d69423596 | |
Abbas Cyclewala | 712b39256c | |
James Cooper | d7ba03040d | |
dependabot[bot] | 083a42489b | |
dependabot[bot] | b8cc8cbbb1 | |
Abbas Cyclewala | 571490455c | |
Charlie King | 99bad9ffff | |
Abbas Cyclewala | 4a2b345fe9 | |
Abbas Cyclewala | fa9c512c49 | |
Abbas Cyclewala | 60b6561f27 | |
Anton Kheystver | 53213bf13e | |
Abbas Cyclewala | 108fa91968 | |
dependabot[bot] | e6624621df | |
dependabot[bot] | 4afdce6c49 | |
Selman DADAK | bc1b70d1cf | |
Selman DADAK | e8ba36cce5 | |
dependabot[bot] | 7a90b63b22 | |
bavardha | 12de7a1437 | |
Abbas Cyclewala | 6138b9c749 | |
Alex Reich | fb2e19da7c | |
Abbas Cyclewala | 10df53da8a | |
Alex Reich | 2af72285bc | |
Abbas Cyclewala | 1449d05810 | |
Alex Reich | 7b089a8260 | |
Jason Finch | 548faba2a8 | |
dependabot[bot] | be8a91d651 | |
Alex Reich | 34f77ed2ec | |
Aleksandar Ivanov | fe38ed5c9f | |
dependabot[bot] | 3ea7fdac96 | |
dependabot[bot] | c4d5bbbae4 | |
Alex Reich | f9def1c6f7 | |
Alex Reich | 4796cbfd70 | |
Alex Reich | 9f898b703b | |
Abbas Cyclewala | b763f718bc | |
dependabot[bot] | bafbff281d | |
Abbas Cyclewala | ec172c9b0f | |
Abbas Cyclewala | 1a1cb540f4 | |
dependabot[bot] | fec029f12e | |
dependabot[bot] | f263ba61d0 | |
dependabot[bot] | 6f1aed0e4d | |
Ashish Prasad | 77ed54aca7 | |
UP | 3a5e31571f | |
dependabot[bot] | fcf172d6df | |
dependabot[bot] | 443cd7a9c9 | |
dependabot[bot] | 09be422b94 | |
dependabot[bot] | 21d7848ec7 | |
dependabot[bot] | 1fe082a95f | |
dependabot[bot] | f3096f9aaa | |
dependabot[bot] | 8a42c5154e | |
dependabot[bot] | cc5e0c59b1 | |
Aleksandar Ivanov | d8b54e7524 | |
Martin | 6e66784239 | |
IchHabeKeineNamen | ebf24933cf | |
Abbas Cyclewala | 5e36760aed | |
Abbas Cyclewala | 0e8a38ea07 | |
Abbas Cyclewala | 331776d3e7 |
|
@ -3,7 +3,7 @@
|
|||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-reportgenerator-globaltool": {
|
||||
"version": "4.7.1",
|
||||
"version": "5.1.23",
|
||||
"commands": [
|
||||
"reportgenerator"
|
||||
]
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "RulesEngine Codespace",
|
||||
"image": "mcr.microsoft.com/vscode/devcontainers/dotnet:0-6.0",
|
||||
"settings": {
|
||||
"terminal.integrated.defaultProfile.linux": "bash"
|
||||
},
|
||||
"extensions": [
|
||||
"eamodio.gitlens",
|
||||
"ms-dotnettools.csharp",
|
||||
"VisualStudioExptTeam.vscodeintellicode",
|
||||
"ms-vscode.powershell",
|
||||
"cschleiden.vscode-github-actions",
|
||||
"redhat.vscode-yaml",
|
||||
"bierner.markdown-preview-github-styles",
|
||||
"coenraads.bracket-pair-colorizer",
|
||||
"vscode-icons-team.vscode-icons",
|
||||
"editorconfig.editorconfig",
|
||||
"aliasadidev.nugetpackagemanagergui",
|
||||
"formulahendry.dotnet-test-explorer"
|
||||
],
|
||||
"postCreateCommand": "dotnet restore RulesEngine.sln && dotnet build RulesEngine.sln --configuration Release --no-restore && dotnet test RulesEngine.sln --configuration Release --no-build --verbosity minimal",
|
||||
"features": {
|
||||
"powershell": "7.1"
|
||||
},
|
||||
}
|
||||
// Built with ❤ by [Pipeline Foundation](https://pipeline.foundation)
|
|
@ -0,0 +1,28 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
# default location of `.github/workflows`
|
||||
directory: "/"
|
||||
open-pull-requests-limit: 3
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
# assignees:
|
||||
# - assignee_one
|
||||
# reviewers:
|
||||
# - reviewer_one
|
||||
|
||||
- package-ecosystem: "nuget"
|
||||
# location of package manifests
|
||||
directory: "/"
|
||||
open-pull-requests-limit: 3
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-minor"]
|
||||
# assignees:
|
||||
# - assignee_one
|
||||
# reviewers:
|
||||
# - reviewer_one
|
||||
|
||||
# Built with ❤ by [Pipeline Foundation](https://pipeline.foundation)
|
|
@ -1,42 +1,56 @@
|
|||
name: "Code scanning"
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
- cron: '0 18 * * 1'
|
||||
- cron: '22 15 * * 4'
|
||||
|
||||
jobs:
|
||||
CodeQL-Build:
|
||||
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'csharp' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: csharp
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
@ -50,4 +64,5 @@ jobs:
|
|||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
||||
|
|
|
@ -8,35 +8,13 @@ on:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# extract branch name
|
||||
- name: Extract branch name
|
||||
if: github.event_name != 'pull_request'
|
||||
shell: bash
|
||||
run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_ENV
|
||||
id: extract_branch
|
||||
|
||||
# extract branch name on pull request
|
||||
- name: Extract branch name on pull request
|
||||
if: github.event_name == 'pull_request'
|
||||
run: echo "BRANCH_NAME=$(echo ${GITHUB_HEAD_REF})" >> $GITHUB_ENV
|
||||
|
||||
# print branch name
|
||||
- name: Get branch name
|
||||
run: echo "The branch name is ${{ env.BRANCH_NAME }}"
|
||||
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v1
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 3.1
|
||||
|
||||
- name: Install minicover
|
||||
run: dotnet tool install --global minicover --version 3.0.6
|
||||
dotnet-version: 6.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore RulesEngine.sln
|
||||
|
@ -44,19 +22,20 @@ jobs:
|
|||
- name: Build
|
||||
run: dotnet build RulesEngine.sln --configuration Release --no-restore
|
||||
|
||||
- name: Instrument
|
||||
run: minicover instrument
|
||||
|
||||
- name: Test
|
||||
run: dotnet test RulesEngine.sln --no-build --configuration Release --verbosity m
|
||||
run: dotnet test RulesEngine.sln --collect:"XPlat Code Coverage" --no-build --configuration Release --verbosity m
|
||||
|
||||
- name: Generate Report
|
||||
shell: pwsh
|
||||
run: ./scripts/generate-coverage-report.ps1
|
||||
|
||||
- name: Check Coverage
|
||||
shell: pwsh
|
||||
run: ./scripts/check-coverage.ps1 -reportPath coveragereport/Cobertura.xml -threshold 96
|
||||
|
||||
- name: Uninstrument
|
||||
run: minicover uninstrument
|
||||
|
||||
- name: Report
|
||||
run: minicover report --threshold 95
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
- name: Report coveralls
|
||||
run: minicover coverallsreport --repo-token ${{ secrets.COVERALLS_TOKEN }} --branch ${{ env.BRANCH_NAME }}
|
||||
- name: Coveralls GitHub Action
|
||||
uses: coverallsapp/github-action@v2.2.1
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
path-to-lcov: ./coveragereport/lcov.info
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
name: Nuget Publish to github
|
||||
on:
|
||||
[workflow_dispatch]
|
||||
jobs:
|
||||
publish:
|
||||
name: nuget publish to github packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: 3.1.x
|
||||
- name: Pack
|
||||
run: dotnet pack src/RulesEngine/RulesEngine.csproj --configuration Release
|
||||
- name: Publish
|
||||
run: find ./src/RulesEngine/bin/Release -iname "*.nupkg" | xargs dotnet nuget push -s https://nuget.pkg.github.com/microsoft/index.json -k ${{secrets.GITHUB_TOKEN}}
|
|
@ -1,49 +0,0 @@
|
|||
name: Nuget Publish
|
||||
on:
|
||||
[workflow_dispatch]
|
||||
jobs:
|
||||
publish:
|
||||
name: build, pack & publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: 3.1.x
|
||||
|
||||
# Publish
|
||||
- name: publish on version change
|
||||
id: publish_nuget
|
||||
uses: rohith/publish-nuget@v2
|
||||
with:
|
||||
# Filepath of the project to be packaged, relative to root of repository
|
||||
PROJECT_FILE_PATH: src/RulesEngine/RulesEngine.csproj
|
||||
|
||||
# NuGet package id, used for version detection & defaults to project name
|
||||
# PACKAGE_NAME: Core
|
||||
|
||||
# Filepath with version info, relative to root of repository & defaults to PROJECT_FILE_PATH
|
||||
# VERSION_FILE_PATH: Directory.Build.props
|
||||
|
||||
# Regex pattern to extract version info in a capturing group
|
||||
# VERSION_REGEX: ^\s*<Version>(.*)<\/Version>\s*$
|
||||
|
||||
# Useful with external providers like Nerdbank.GitVersioning, ignores VERSION_FILE_PATH & VERSION_REGEX
|
||||
# VERSION_STATIC: 1.0.0
|
||||
|
||||
# Flag to toggle git tagging, enabled by default
|
||||
# TAG_COMMIT: true
|
||||
|
||||
# Format of the git tag, [*] gets replaced with actual version
|
||||
# TAG_FORMAT: v*
|
||||
|
||||
# API key to authenticate with NuGet server
|
||||
NUGET_KEY: ${{secrets.NUGET_API_KEY}}
|
||||
|
||||
# NuGet server uri hosting the packages, defaults to https://api.nuget.org
|
||||
# NUGET_SOURCE: https://api.nuget.org
|
||||
|
||||
# Flag to toggle pushing symbols along with nuget package to the server, disabled by default
|
||||
# INCLUDE_SYMBOLS: false
|
|
@ -330,4 +330,8 @@ ASALocalRun/
|
|||
.mfractor/
|
||||
/src/RulesEngine/RulesEngine.sln.licenseheader
|
||||
/assets/RulesEnginePackageFile.xml
|
||||
coveragereport/
|
||||
coveragereport/
|
||||
|
||||
src/**/*.snk
|
||||
|
||||
dist
|
94
CHANGELOG.md
94
CHANGELOG.md
|
@ -2,22 +2,100 @@
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [3.1.0-preview.3]
|
||||
- Fixed scoped parameters runtime errors not logging as errorMessage
|
||||
## [5.0.3]
|
||||
- Updated dependencies to latest
|
||||
- Fixed RulesEngine throwing exception when type name is same as input name
|
||||
- Added config to disable FastCompile for expressions
|
||||
- Added RuleParameter.Create method for better handling on types when value is null
|
||||
|
||||
## [3.1.0-preview.2]
|
||||
- Runtime errors for expressions will now be logged as errorMessage instead of throwing Exceptions by default
|
||||
## [5.0.2]
|
||||
- Fixed Scoped Params returning incorrect results in some corner case scenarios
|
||||
|
||||
## [3.1.0-preview.1]
|
||||
## [5.0.1]
|
||||
- Added option to disable automatic type registry for input parameters in reSettings
|
||||
- Added option to make expression case sensitive in reSettings
|
||||
|
||||
## [5.0.0]
|
||||
- Fixed security bug related to System.Dynamic.Linq.Core
|
||||
|
||||
### Breaking Changes
|
||||
- As a part of security bug fix, method call for only registered types via reSettings will be allowed. This only impacts strongly typed inputs and nested types
|
||||
|
||||
|
||||
## [4.0.0]
|
||||
- RulesEngine is now available in both dotnet 6 and netstandard 2.0
|
||||
- Dependency on ILogger, MemoryCache have been removed
|
||||
- Obsolete Properties and Methods have been removed
|
||||
- Fixed name of RuleParameter is ignored if the type is recognized (by @peeveen)
|
||||
### Breaking Changes
|
||||
- ILogger has been removed from RulesEngine and all its constructors
|
||||
```diff
|
||||
- RulesEngine(string[] jsonConfig, ILogger logger = null, ReSettings reSettings = null)
|
||||
+ RulesEngine(string[] jsonConfig, ReSettings reSettings = null)
|
||||
|
||||
- RulesEngine(Workflow[] Workflows, ILogger logger = null, ReSettings reSettings = null)
|
||||
+ RulesEngine(Workflow[] Workflows, ReSettings reSettings = null)
|
||||
|
||||
- RulesEngine(ILogger logger = null, ReSettings reSettings = null)
|
||||
+ RulesEngine(ReSettings reSettings = null)
|
||||
```
|
||||
- Obsolete methods and properties have been removed, from the follow models:-
|
||||
- RuleResultTree
|
||||
- `ToResultTreeMessages()` has been removed from `RuleResultTree` model
|
||||
- `GetMessages()` has been removed from `RuleResultTree` model
|
||||
- `RuleEvaluatedParams` has been removed from `RuleResultTree` model, Please use `Inputs` instead
|
||||
|
||||
- Workflow
|
||||
- `WorkflowRulesToInject` has been removed, Please use `WorkflowsToInject` instead
|
||||
- `ErrorType` has been removed from `Rule`
|
||||
|
||||
- Resettings
|
||||
- `EnableLocalParams` has been removed from `ReSettings`, Please use `EnableScopedParams` instead
|
||||
|
||||
|
||||
## [3.5.0]
|
||||
- `EvaluateRule` action now support custom inputs and filtered inputs
|
||||
- Added `ContainsWorkflow` method in RulesEngine (by @okolobaxa)
|
||||
- Fixed minor bugs (#258 & #259)
|
||||
|
||||
## [3.4.0]
|
||||
- Made RulesEngine Strong Name and Authenticode signed
|
||||
- Renamed few models to streamline names (by @alexrich)
|
||||
- `WorkflowRules` is renamed to `Workflow`
|
||||
- `WorkflowRulesToInject` is renamed to `WorkflowsToInject`
|
||||
- `RuleAction` is renamed to `RuleActions`
|
||||
|
||||
**Note**: The old models are still supported but will be removed with version 4.0.0
|
||||
|
||||
|
||||
## [3.3.0]
|
||||
- Added support for actions in nested rules
|
||||
- Improved serialization support for System.Text.Json for workflow model
|
||||
|
||||
Breaking Change:
|
||||
- Type of Action has been changed from `Dictionary<ActionTriggerType, ActionInfo>` to `RuleActions`
|
||||
- No impact if you are serializing workflow from json
|
||||
- For workflow objects created in code, refer - [link](https://github.com/microsoft/RulesEngine/pull/182/files#diff-a5093dda2dcc1e4958ce3533edb607bb61406e1f0a9071eca4e317bdd987c0d3)
|
||||
|
||||
## [3.2.0]
|
||||
- Added AddOrUpdateWorkflow method to update workflows atomically (by @AshishPrasad)
|
||||
- Updated dependencies to latest
|
||||
|
||||
Breaking Change:
|
||||
- `AddWorkflow` now throws exception if you try to add a workflow which already exists.
|
||||
Use `AddOrUpdateWorkflow` to update existing workflow
|
||||
|
||||
## [3.1.0]
|
||||
- Added globalParams feature which can be applied to all rules
|
||||
- Enabled localParams support for nested Rules
|
||||
- Made certain fields in Rule model optional allowing users to define workflow with minimal fields
|
||||
- Added option to disable Rule in workflow json
|
||||
- Added `GetAllRegisteredWorkflow` to RulesEngine to return all registeredWorkflows
|
||||
- Fixed Rule compilation exception not returned when Rule has ErrorMessage field defined - #95
|
||||
- Added `GetAllRegisteredWorkflow` to RulesEngine to return all registered workflows
|
||||
- Runtime errors for expressions will now be logged as errorMessage instead of throwing Exceptions by default
|
||||
- Fixed RuleParameter passed as null
|
||||
|
||||
## [3.0.2]
|
||||
- Fixed LocalParams cache not getting cleaned up when RemoveWorkflow and ClearWorkflows are called
|
||||
- Fixed LocalParams cache not getting cleaned up when RemoveWorkflows and ClearWorkflows are called
|
||||
|
||||
## [3.0.1]
|
||||
- Moved ActionResult and ActionRuleResult under RulesEngine.Models namespace
|
||||
|
|
89
README.md
89
README.md
|
@ -5,17 +5,23 @@
|
|||
|
||||
[download-image]: https://img.shields.io/nuget/dt/RulesEngine
|
||||
[download-url]: https://www.nuget.org/packages/RulesEngine/
|
||||
|
||||
## Overview
|
||||
Rules Engine is a library/NuGet package for abstracting business logic/rules/policies out of the system. This works in a very simple way by giving you an ability to put your rules in a store outside the core logic of the system thus ensuring that any change in rules doesn't affect the core system.
|
||||
|
||||
Rules Engine is a library/NuGet package for abstracting business logic/rules/policies out of a system. It provides a simple way of giving you the ability to put your rules in a store outside the core logic of the system, thus ensuring that any change in rules don't affect the core system.
|
||||
|
||||
## Installation
|
||||
To install this library, please download the latest version of [NuGet Package](https://www.nuget.org/packages/RulesEngine/) from [nuget.org](https://www.nuget.org/) and refer it into your project.
|
||||
|
||||
To install this library, download the latest version of [NuGet Package](https://www.nuget.org/packages/RulesEngine/) from [nuget.org](https://www.nuget.org/) and refer it into your project.
|
||||
|
||||
## How to use it
|
||||
|
||||
You need to store the rules based on the [schema definition](https://github.com/microsoft/RulesEngine/blob/main/schema/workflowRules-schema.json) given and they can be stored in any store as deemed appropriate like Azure Blob Storage, Cosmos DB, Azure App Configuration, SQL Servers, file systems etc. The expressions are supposed to be a [lambda expressions](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions).
|
||||
There are several ways to populate workflows for the Rules Engine as listed below.
|
||||
|
||||
You need to store the rules based on the [schema definition](https://github.com/microsoft/RulesEngine/blob/main/schema/workflow-schema.json) given and they can be stored in any store as deemed appropriate like Azure Blob Storage, Cosmos DB, Azure App Configuration, [Entity Framework](https://github.com/microsoft/RulesEngine#entity-framework), SQL Servers, file systems etc. For RuleExpressionType `LambdaExpression`, the rule is written as a [lambda expressions](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions).
|
||||
|
||||
An example rule:
|
||||
|
||||
An example rule could be -
|
||||
```json
|
||||
[
|
||||
{
|
||||
|
@ -27,7 +33,7 @@ An example rule could be -
|
|||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.country == \"india\" AND input1.loyalityFactor <= 2 AND input1.totalPurchasesToDate >= 5000"
|
||||
"Expression": "input1.country == \"india\" AND input1.loyaltyFactor <= 2 AND input1.totalPurchasesToDate >= 5000"
|
||||
},
|
||||
{
|
||||
"RuleName": "GiveDiscount20",
|
||||
|
@ -35,7 +41,7 @@ An example rule could be -
|
|||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.country == \"india\" AND input1.loyalityFactor >= 3 AND input1.totalPurchasesToDate >= 10000"
|
||||
"Expression": "input1.country == \"india\" AND input1.loyaltyFactor >= 3 AND input1.totalPurchasesToDate >= 10000"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -43,35 +49,83 @@ An example rule could be -
|
|||
```
|
||||
|
||||
You can inject the rules into the Rules Engine by initiating an instance by using the following code -
|
||||
```c#
|
||||
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 ExecuteAllRulesAsync as shown below -
|
||||
```c#
|
||||
var rulesEngine = new RulesEngine(workflow);
|
||||
```
|
||||
Here, *workflow* is a list of deserialized objects based on the schema explained above
|
||||
Once initialised, the Rules Engine needs to execute the rules for a given input. This can be done by calling the method `ExecuteAllRulesAsync`:
|
||||
|
||||
```c#
|
||||
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.
|
||||
|
||||
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, which itself may consist of a list of class instances.
|
||||
|
||||
The *response* will contain a list of [*RuleResultTree*](https://github.com/microsoft/RulesEngine/wiki/Getting-Started#ruleresulttree) which gives information if a particular rule passed or failed.
|
||||
|
||||
|
||||
_Note: A detailed example showcasing how to use Rules Engine is explained in [Getting Started page](https://github.com/microsoft/RulesEngine/wiki/Getting-Started) of [Rules Engine Wiki](https://github.com/microsoft/RulesEngine/wiki)._
|
||||
|
||||
_A demo app for the is available at [this location](https://github.com/microsoft/RulesEngine/tree/main/demo)._
|
||||
|
||||
### Basic
|
||||
|
||||
A simple example via code only is as follows:
|
||||
|
||||
```c#
|
||||
List<Rule> rules = new List<Rule>();
|
||||
|
||||
Rule rule = new Rule();
|
||||
rule.RuleName = "Test Rule";
|
||||
rule.SuccessEvent = "Count is within tolerance.";
|
||||
rule.ErrorMessage = "Over expected.";
|
||||
rule.Expression = "count < 3";
|
||||
rule.RuleExpressionType = RuleExpressionType.LambdaExpression;
|
||||
rules.Add(rule);
|
||||
|
||||
var workflows = new List<Workflow>();
|
||||
|
||||
Workflow exampleWorkflow = new Workflow();
|
||||
exampleWorkflow.WorkflowName = "Example Workflow";
|
||||
exampleWorkflow.Rules = rules;
|
||||
|
||||
workflows.Add(exampleWorkflow);
|
||||
|
||||
var bre = new RulesEngine.RulesEngine(workflows.ToArray());
|
||||
```
|
||||
### Entity Framework
|
||||
|
||||
Consuming Entity Framework and populating the Rules Engine is shown in the [EFDemo class](https://github.com/microsoft/RulesEngine/blob/main/demo/DemoApp/EFDemo.cs) with Workflow rules populating the array and passed to the Rules Engine, The Demo App includes an example [RulesEngineDemoContext](https://github.com/microsoft/RulesEngine/blob/main/demo/DemoApp.EFDataExample/RulesEngineDemoContext.cs) using SQLite and could be swapped out for another provider.
|
||||
|
||||
```c#
|
||||
var wfr = db.Workflows.Include(i => i.Rules).ThenInclude(i => i.Rules).ToArray();
|
||||
var bre = new RulesEngine.RulesEngine(wfr, null);
|
||||
```
|
||||
|
||||
*Note: For each level of nested rules expected, a ThenInclude query appended will be needed as shown above.*
|
||||
|
||||
## How it works
|
||||
|
||||
![](https://github.com/microsoft/RulesEngine/blob/main/assets/BlockDiagram.png)
|
||||
|
||||
The rules can be stored in any store and be fed to the system in a structure which follows a proper [schema](https://github.com/microsoft/RulesEngine/blob/main/schema/workflowRules-schema.json) of WorkFlow model.
|
||||
|
||||
The wrapper needs to be created over the Rules Engine package, which will get the rules and input message(s) from any store that your system dictates and put it into the Engine. Also, the wrapper then needs to handle the output using appropriate means.
|
||||
The rules can be stored in any store and be fed to the system in a structure which adheres to the [schema](https://github.com/microsoft/RulesEngine/blob/main/schema/workflow-schema.json) of WorkFlow model.
|
||||
|
||||
A wrapper needs to be created over the Rules Engine package, which will get the rules and input message(s) from any store that your system dictates and put it into the Engine. The wrapper then handles the output using appropriate means.
|
||||
|
||||
_Note: To know in detail of the workings of Rules Engine, please visit [How it works section](https://github.com/microsoft/RulesEngine/wiki/Introduction#how-it-works) in [Rules Engine Wiki](https://github.com/microsoft/RulesEngine/wiki)._
|
||||
|
||||
## 3rd Party Tools
|
||||
|
||||
### RulesEngine Editor
|
||||
There is an editor library with it's own [NuGet Package](https://www.nuget.org/packages/RulesEngineEditor/) written in Blazor, more information is in it's repo https://github.com/alexreich/RulesEngineEditor.
|
||||
|
||||
#### Live Demo
|
||||
https://alexreich.github.io/RulesEngineEditor
|
||||
> This can also be installed as a standalone PWA and used offline.
|
||||
|
||||
#### With Sample Data
|
||||
https://alexreich.github.io/RulesEngineEditor/demo
|
||||
|
||||
## Contributing
|
||||
|
||||
This project welcomes contributions and suggestions. Most contributions require you to agree to a
|
||||
|
@ -82,9 +136,6 @@ This project has adopted the [Microsoft Open Source Code of Conduct](https://ope
|
|||
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
||||
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
_For more details please check out [Rules Engine Wiki](https://github.com/microsoft/RulesEngine/wiki)._
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29123.89
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31717.71
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RulesEngine", "src\RulesEngine\RulesEngine.csproj", "{CD4DFE6A-083B-478E-8377-77F474833E30}"
|
||||
EndProject
|
||||
|
@ -18,6 +18,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||
CHANGELOG.md = CHANGELOG.md
|
||||
global.json = global.json
|
||||
README.md = README.md
|
||||
schema\workflow-list-schema.json = schema\workflow-list-schema.json
|
||||
schema\workflow-schema.json = schema\workflow-schema.json
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RulesEngineBenchmark", "benchmark\RulesEngineBenchmark\RulesEngineBenchmark.csproj", "{C058809F-C720-4EFC-925D-A486627B238B}"
|
||||
|
@ -25,6 +27,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RulesEngineBenchmark", "ben
|
|||
{CD4DFE6A-083B-478E-8377-77F474833E30} = {CD4DFE6A-083B-478E-8377-77F474833E30}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DemoApp.EFDataExample", "demo\DemoApp.EFDataExample\DemoApp.EFDataExample.csproj", "{E376D3E6-6890-4C09-9EA0-3EFD9C1E036D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -47,6 +51,10 @@ Global
|
|||
{C058809F-C720-4EFC-925D-A486627B238B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C058809F-C720-4EFC-925D-A486627B238B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C058809F-C720-4EFC-925D-A486627B238B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E376D3E6-6890-4C09-9EA0-3EFD9C1E036D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E376D3E6-6890-4C09-9EA0-3EFD9C1E036D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E376D3E6-6890-4C09-9EA0-3EFD9C1E036D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E376D3E6-6890-4C09-9EA0-3EFD9C1E036D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.5 BLOCK -->
|
||||
|
||||
## Security
|
||||
|
||||
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
|
||||
|
||||
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below.
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues.**
|
||||
|
||||
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report).
|
||||
|
||||
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).
|
||||
|
||||
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
|
||||
|
||||
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
|
||||
|
||||
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
|
||||
* Full paths of source file(s) related to the manifestation of the issue
|
||||
* The location of the affected source code (tag/branch/commit or direct URL)
|
||||
* Any special configuration required to reproduce the issue
|
||||
* Step-by-step instructions to reproduce the issue
|
||||
* Proof-of-concept or exploit code (if possible)
|
||||
* Impact of the issue, including how an attacker might exploit the issue
|
||||
|
||||
This information will help us triage your report more quickly.
|
||||
|
||||
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs.
|
||||
|
||||
## Preferred Languages
|
||||
|
||||
We prefer all communications to be in English.
|
||||
|
||||
## Policy
|
||||
|
||||
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).
|
||||
|
||||
<!-- END MICROSOFT SECURITY.MD BLOCK -->
|
|
@ -16,7 +16,7 @@ namespace RulesEngineBenchmark
|
|||
{
|
||||
private readonly RulesEngine.RulesEngine rulesEngine;
|
||||
private readonly object ruleInput;
|
||||
private readonly List<WorkflowRules> workflows;
|
||||
private readonly List<Workflow> workflow;
|
||||
|
||||
private class ListItem
|
||||
{
|
||||
|
@ -34,9 +34,9 @@ namespace RulesEngineBenchmark
|
|||
}
|
||||
|
||||
var fileData = File.ReadAllText(files[0]);
|
||||
workflows = JsonConvert.DeserializeObject<List<WorkflowRules>>(fileData);
|
||||
workflow = JsonConvert.DeserializeObject<List<Workflow>>(fileData);
|
||||
|
||||
rulesEngine = new RulesEngine.RulesEngine(workflows.ToArray(), null, new ReSettings {
|
||||
rulesEngine = new RulesEngine.RulesEngine(workflow.ToArray(), new ReSettings {
|
||||
EnableFormattedErrorMessage = false,
|
||||
EnableScopedParams = false
|
||||
});
|
||||
|
@ -69,7 +69,7 @@ namespace RulesEngineBenchmark
|
|||
[Benchmark]
|
||||
public void RuleExecutionDefault()
|
||||
{
|
||||
foreach (var workflow in workflows)
|
||||
foreach (var workflow in workflow)
|
||||
{
|
||||
_ = rulesEngine.ExecuteAllRulesAsync(workflow.WorkflowName, ruleInput).Result;
|
||||
}
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
|
||||
<!--<PackageReference Include="RulesEngine" Version="3.0.2" />-->
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.country == \"india\" AND input1.loyalityFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2"
|
||||
"Expression": "input1.country == \"india\" AND input1.loyaltyFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2"
|
||||
},
|
||||
{
|
||||
"RuleName": "GiveDiscount20",
|
||||
|
@ -16,7 +16,7 @@
|
|||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.country == \"india\" AND input1.loyalityFactor == 3 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2"
|
||||
"Expression": "input1.country == \"india\" AND input1.loyaltyFactor == 3 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2"
|
||||
},
|
||||
{
|
||||
"RuleName": "GiveDiscount25",
|
||||
|
@ -24,7 +24,7 @@
|
|||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.country != \"india\" AND input1.loyalityFactor >= 2 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 5"
|
||||
"Expression": "input1.country != \"india\" AND input1.loyaltyFactor >= 2 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 5"
|
||||
},
|
||||
{
|
||||
"RuleName": "GiveDiscount30",
|
||||
|
@ -32,7 +32,7 @@
|
|||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.loyalityFactor > 3 AND input1.totalPurchasesToDate >= 50000 AND input1.totalPurchasesToDate <= 100000 AND input2.totalOrders > 5 AND input3.noOfVisitsPerMonth > 15"
|
||||
"Expression": "input1.loyaltyFactor > 3 AND input1.totalPurchasesToDate >= 50000 AND input1.totalPurchasesToDate <= 100000 AND input2.totalOrders > 5 AND input3.noOfVisitsPerMonth > 15"
|
||||
},
|
||||
{
|
||||
"RuleName": "GiveDiscount30NestedOrExample",
|
||||
|
@ -46,7 +46,7 @@
|
|||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.loyalityFactor > 3 AND input1.totalPurchasesToDate >= 50000 AND input1.totalPurchasesToDate <= 100000"
|
||||
"Expression": "input1.loyaltyFactor > 3 AND input1.totalPurchasesToDate >= 50000 AND input1.totalPurchasesToDate <= 100000"
|
||||
},
|
||||
{
|
||||
"RuleName": "OrHasHighNumberOfTotalOrders",
|
||||
|
@ -69,7 +69,7 @@
|
|||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.loyalityFactor > 3"
|
||||
"Expression": "input1.loyaltyFactor > 3"
|
||||
},
|
||||
{
|
||||
"RuleName": "AndHasTotalPurchased100000",
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RootNamespace>DemoApp.EFDataExample</RootNamespace>
|
||||
<AssemblyName>DemoApp.EFDataExample</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\RulesEngine\RulesEngine.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,59 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using RulesEngine.Models;
|
||||
|
||||
namespace RulesEngine.Data
|
||||
{
|
||||
public class RulesEngineContext : DbContext
|
||||
{
|
||||
public DbSet<Workflow> Workflows { get; set; }
|
||||
|
||||
public DbSet<Rule> Rules { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<ScopedParam>()
|
||||
.HasKey(k => k.Name);
|
||||
|
||||
modelBuilder.Entity<Workflow>(entity => {
|
||||
entity.HasKey(k => k.WorkflowName);
|
||||
entity.Ignore(b => b.WorkflowsToInject);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Rule>().HasOne<Rule>().WithMany(r => r.Rules).HasForeignKey("RuleNameFK");
|
||||
|
||||
var serializationOptions = new JsonSerializerOptions(JsonSerializerDefaults.General);
|
||||
|
||||
modelBuilder.Entity<Rule>(entity => {
|
||||
entity.HasKey(k => k.RuleName);
|
||||
|
||||
var valueComparer = new ValueComparer<Dictionary<string, object>>(
|
||||
(c1, c2) => c1.SequenceEqual(c2),
|
||||
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
|
||||
c => c);
|
||||
|
||||
entity.Property(b => b.Properties)
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, serializationOptions),
|
||||
v => JsonSerializer.Deserialize<Dictionary<string, object>>(v, serializationOptions))
|
||||
.Metadata
|
||||
.SetValueComparer(valueComparer);
|
||||
|
||||
entity.Property(p => p.Actions)
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, serializationOptions),
|
||||
v => JsonSerializer.Deserialize<RuleActions>(v, serializationOptions));
|
||||
|
||||
entity.Ignore(b => b.WorkflowsToInject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RulesEngine.Data;
|
||||
using RulesEngine.Models;
|
||||
|
||||
namespace DemoApp.EFDataExample
|
||||
{
|
||||
public class RulesEngineDemoContext : RulesEngineContext
|
||||
{
|
||||
public string DbPath { get; private set; }
|
||||
|
||||
public RulesEngineDemoContext()
|
||||
{
|
||||
var folder = Environment.SpecialFolder.LocalApplicationData;
|
||||
var path = Environment.GetFolderPath(folder);
|
||||
DbPath = $"{path}{System.IO.Path.DirectorySeparatorChar}RulesEngineDemo.db";
|
||||
}
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder options)
|
||||
=> options.UseSqlite($"Data Source={DbPath}");
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,13 +1,10 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using RulesEngine.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Dynamic;
|
||||
using System.IO;
|
||||
using static RulesEngine.Extensions.ListofRuleResultTreeExtension;
|
||||
|
||||
namespace DemoApp
|
||||
|
@ -17,45 +14,51 @@ namespace DemoApp
|
|||
public void Run()
|
||||
{
|
||||
Console.WriteLine($"Running {nameof(BasicDemo)}....");
|
||||
var basicInfo = "{\"name\": \"hello\",\"email\": \"abcy@xyz.com\",\"creditHistory\": \"good\",\"country\": \"canada\",\"loyalityFactor\": 3,\"totalPurchasesToDate\": 10000}";
|
||||
var orderInfo = "{\"totalOrders\": 5,\"recurringItems\": 2}";
|
||||
var telemetryInfo = "{\"noOfVisitsPerMonth\": 10,\"percentageOfBuyingToVisit\": 15}";
|
||||
List<Workflow> workflows = new List<Workflow>();
|
||||
Workflow workflow = new Workflow();
|
||||
workflow.WorkflowName = "Test Workflow Rule 1";
|
||||
|
||||
var converter = new ExpandoObjectConverter();
|
||||
List<Rule> rules = new List<Rule>();
|
||||
|
||||
dynamic input1 = JsonConvert.DeserializeObject<ExpandoObject>(basicInfo, converter);
|
||||
dynamic input2 = JsonConvert.DeserializeObject<ExpandoObject>(orderInfo, converter);
|
||||
dynamic input3 = JsonConvert.DeserializeObject<ExpandoObject>(telemetryInfo, converter);
|
||||
Rule rule = new Rule();
|
||||
rule.RuleName = "Test Rule";
|
||||
rule.SuccessEvent = "Count is within tolerance.";
|
||||
rule.ErrorMessage = "Over expected.";
|
||||
rule.Expression = "count < 3";
|
||||
rule.RuleExpressionType = RuleExpressionType.LambdaExpression;
|
||||
|
||||
rules.Add(rule);
|
||||
|
||||
workflow.Rules = rules;
|
||||
|
||||
workflows.Add(workflow);
|
||||
|
||||
var bre = new RulesEngine.RulesEngine(workflows.ToArray(), null);
|
||||
|
||||
dynamic datas = new ExpandoObject();
|
||||
datas.count = 1;
|
||||
var inputs = new dynamic[]
|
||||
{
|
||||
input1,
|
||||
input2,
|
||||
input3
|
||||
};
|
||||
{
|
||||
datas
|
||||
};
|
||||
|
||||
var files = Directory.GetFiles(Directory.GetCurrentDirectory(), "Discount.json", SearchOption.AllDirectories);
|
||||
if (files == null || files.Length == 0)
|
||||
throw new Exception("Rules not found.");
|
||||
List<RuleResultTree> resultList = bre.ExecuteAllRulesAsync("Test Workflow Rule 1", inputs).Result;
|
||||
|
||||
var fileData = File.ReadAllText(files[0]);
|
||||
var workflowRules = JsonConvert.DeserializeObject<List<WorkflowRules>>(fileData);
|
||||
bool outcome = false;
|
||||
|
||||
var bre = new RulesEngine.RulesEngine(workflowRules.ToArray(), null);
|
||||
|
||||
string discountOffered = "No discount offered.";
|
||||
|
||||
List<RuleResultTree> resultList = bre.ExecuteAllRulesAsync("Discount", inputs).Result;
|
||||
//Different ways to show test results:
|
||||
outcome = resultList.TrueForAll(r => r.IsSuccess);
|
||||
|
||||
resultList.OnSuccess((eventName) => {
|
||||
discountOffered = $"Discount offered is {eventName} % over MRP.";
|
||||
Console.WriteLine($"Result '{eventName}' is as expected.");
|
||||
outcome = true;
|
||||
});
|
||||
|
||||
resultList.OnFail(() => {
|
||||
discountOffered = "The user is not eligible for any discount.";
|
||||
outcome = false;
|
||||
});
|
||||
|
||||
Console.WriteLine(discountOffered);
|
||||
Console.WriteLine($"Test outcome: {outcome}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<StartupObject>DemoApp.Program</StartupObject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<ProjectReference Include="../../src/RulesEngine/RulesEngine.csproj" />
|
||||
<ProjectReference Include="..\DemoApp.EFDataExample\DemoApp.EFDataExample.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using DemoApp.EFDataExample;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using RulesEngine.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Dynamic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using static RulesEngine.Extensions.ListofRuleResultTreeExtension;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DemoApp
|
||||
{
|
||||
public class EFDemo
|
||||
{
|
||||
public void Run()
|
||||
{
|
||||
Console.WriteLine($"Running {nameof(EFDemo)}....");
|
||||
var basicInfo = "{\"name\": \"hello\",\"email\": \"abcy@xyz.com\",\"creditHistory\": \"good\",\"country\": \"canada\",\"loyaltyFactor\": 3,\"totalPurchasesToDate\": 10000}";
|
||||
var orderInfo = "{\"totalOrders\": 5,\"recurringItems\": 2}";
|
||||
var telemetryInfo = "{\"noOfVisitsPerMonth\": 10,\"percentageOfBuyingToVisit\": 15}";
|
||||
|
||||
var converter = new ExpandoObjectConverter();
|
||||
|
||||
dynamic input1 = JsonConvert.DeserializeObject<ExpandoObject>(basicInfo, converter);
|
||||
dynamic input2 = JsonConvert.DeserializeObject<ExpandoObject>(orderInfo, converter);
|
||||
dynamic input3 = JsonConvert.DeserializeObject<ExpandoObject>(telemetryInfo, converter);
|
||||
|
||||
var inputs = new dynamic[]
|
||||
{
|
||||
input1,
|
||||
input2,
|
||||
input3
|
||||
};
|
||||
|
||||
var files = Directory.GetFiles(Directory.GetCurrentDirectory(), "Discount.json", SearchOption.AllDirectories);
|
||||
if (files == null || files.Length == 0)
|
||||
throw new Exception("Rules not found.");
|
||||
|
||||
var fileData = File.ReadAllText(files[0]);
|
||||
var workflow = JsonConvert.DeserializeObject<List<Workflow>>(fileData);
|
||||
|
||||
RulesEngineDemoContext db = new RulesEngineDemoContext();
|
||||
if (db.Database.EnsureCreated())
|
||||
{
|
||||
db.Workflows.AddRange(workflow);
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
var wfr = db.Workflows.Include(i => i.Rules).ThenInclude(i => i.Rules).ToArray();
|
||||
|
||||
var bre = new RulesEngine.RulesEngine(wfr, null);
|
||||
|
||||
string discountOffered = "No discount offered.";
|
||||
|
||||
List<RuleResultTree> resultList = bre.ExecuteAllRulesAsync("Discount", inputs).Result;
|
||||
|
||||
resultList.OnSuccess((eventName) => {
|
||||
discountOffered = $"Discount offered is {eventName} % over MRP.";
|
||||
});
|
||||
|
||||
resultList.OnFail(() => {
|
||||
discountOffered = "The user is not eligible for any discount.";
|
||||
});
|
||||
|
||||
Console.WriteLine(discountOffered);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using RulesEngine.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Dynamic;
|
||||
using System.IO;
|
||||
using static RulesEngine.Extensions.ListofRuleResultTreeExtension;
|
||||
|
||||
namespace DemoApp
|
||||
{
|
||||
public class JSONDemo
|
||||
{
|
||||
public void Run()
|
||||
{
|
||||
Console.WriteLine($"Running {nameof(JSONDemo)}....");
|
||||
var basicInfo = "{\"name\": \"hello\",\"email\": \"abcy@xyz.com\",\"creditHistory\": \"good\",\"country\": \"canada\",\"loyaltyFactor\": 3,\"totalPurchasesToDate\": 10000}";
|
||||
var orderInfo = "{\"totalOrders\": 5,\"recurringItems\": 2}";
|
||||
var telemetryInfo = "{\"noOfVisitsPerMonth\": 10,\"percentageOfBuyingToVisit\": 15}";
|
||||
|
||||
var converter = new ExpandoObjectConverter();
|
||||
|
||||
dynamic input1 = JsonConvert.DeserializeObject<ExpandoObject>(basicInfo, converter);
|
||||
dynamic input2 = JsonConvert.DeserializeObject<ExpandoObject>(orderInfo, converter);
|
||||
dynamic input3 = JsonConvert.DeserializeObject<ExpandoObject>(telemetryInfo, converter);
|
||||
|
||||
var inputs = new dynamic[]
|
||||
{
|
||||
input1,
|
||||
input2,
|
||||
input3
|
||||
};
|
||||
|
||||
var files = Directory.GetFiles(Directory.GetCurrentDirectory(), "Discount.json", SearchOption.AllDirectories);
|
||||
if (files == null || files.Length == 0)
|
||||
throw new Exception("Rules not found.");
|
||||
|
||||
var fileData = File.ReadAllText(files[0]);
|
||||
var workflow = JsonConvert.DeserializeObject<List<Workflow>>(fileData);
|
||||
|
||||
var bre = new RulesEngine.RulesEngine(workflow.ToArray(), null);
|
||||
|
||||
string discountOffered = "No discount offered.";
|
||||
|
||||
List<RuleResultTree> resultList = bre.ExecuteAllRulesAsync("Discount", inputs).Result;
|
||||
|
||||
resultList.OnSuccess((eventName) => {
|
||||
discountOffered = $"Discount offered is {eventName} % over MRP.";
|
||||
});
|
||||
|
||||
resultList.OnFail(() => {
|
||||
discountOffered = "The user is not eligible for any discount.";
|
||||
});
|
||||
|
||||
Console.WriteLine(discountOffered);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -49,15 +49,15 @@ namespace DemoApp
|
|||
}
|
||||
|
||||
var fileData = File.ReadAllText(files[0]);
|
||||
var workflowRules = JsonConvert.DeserializeObject<List<WorkflowRules>>(fileData);
|
||||
var Workflows = JsonConvert.DeserializeObject<List<Workflow>>(fileData);
|
||||
|
||||
var bre = new RulesEngine.RulesEngine(workflowRules.ToArray(), null);
|
||||
foreach (var workflow in workflowRules)
|
||||
var bre = new RulesEngine.RulesEngine(Workflows.ToArray(), null);
|
||||
foreach (var workflow in Workflows)
|
||||
{
|
||||
var resultList = bre.ExecuteAllRulesAsync(workflow.WorkflowName, nestedInput).Result;
|
||||
|
||||
resultList.OnSuccess((eventName) => {
|
||||
Console.WriteLine($"{workflow.WorkflowName} evaluation resulted in succees - {eventName}");
|
||||
Console.WriteLine($"{workflow.WorkflowName} evaluation resulted in success - {eventName}");
|
||||
}).OnFail(() => {
|
||||
Console.WriteLine($"{workflow.WorkflowName} evaluation resulted in failure");
|
||||
});
|
||||
|
|
|
@ -8,7 +8,9 @@ namespace DemoApp
|
|||
public static void Main(string[] args)
|
||||
{
|
||||
new BasicDemo().Run();
|
||||
new JSONDemo().Run();
|
||||
new NestedInputDemo().Run();
|
||||
new EFDemo().Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.country == \"india\" AND input1.loyalityFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2"
|
||||
"Expression": "input1.country == \"india\" AND input1.loyaltyFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2"
|
||||
},
|
||||
{
|
||||
"RuleName": "GiveDiscount20",
|
||||
|
@ -16,7 +16,7 @@
|
|||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.country == \"india\" AND input1.loyalityFactor == 3 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2"
|
||||
"Expression": "input1.country == \"india\" AND input1.loyaltyFactor == 3 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2"
|
||||
},
|
||||
{
|
||||
"RuleName": "GiveDiscount25",
|
||||
|
@ -24,7 +24,7 @@
|
|||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.country != \"india\" AND input1.loyalityFactor >= 2 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 5"
|
||||
"Expression": "input1.country != \"india\" AND input1.loyaltyFactor >= 2 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 5"
|
||||
},
|
||||
{
|
||||
"RuleName": "GiveDiscount30",
|
||||
|
@ -32,7 +32,7 @@
|
|||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.loyalityFactor > 3 AND input1.totalPurchasesToDate >= 50000 AND input1.totalPurchasesToDate <= 100000 AND input2.totalOrders > 5 AND input3.noOfVisitsPerMonth > 15"
|
||||
"Expression": "input1.loyaltyFactor > 3 AND input1.totalPurchasesToDate >= 50000 AND input1.totalPurchasesToDate <= 100000 AND input2.totalOrders > 5 AND input3.noOfVisitsPerMonth > 15"
|
||||
},
|
||||
{
|
||||
"RuleName": "GiveDiscount30NestedOrExample",
|
||||
|
@ -46,7 +46,7 @@
|
|||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.loyalityFactor > 3 AND input1.totalPurchasesToDate >= 50000 AND input1.totalPurchasesToDate <= 100000"
|
||||
"Expression": "input1.loyaltyFactor > 3 AND input1.totalPurchasesToDate >= 50000 AND input1.totalPurchasesToDate <= 100000"
|
||||
},
|
||||
{
|
||||
"RuleName": "OrHasHighNumberOfTotalOrders",
|
||||
|
@ -69,7 +69,7 @@
|
|||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.loyalityFactor > 3"
|
||||
"Expression": "input1.loyaltyFactor > 3"
|
||||
},
|
||||
{
|
||||
"RuleName": "AndHasTotalPurchased100000",
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string] $csprojFilePath,
|
||||
[Parameter(Mandatory)]
|
||||
[string] $signingKey
|
||||
)
|
||||
|
||||
# sign and build the project
|
||||
$directory = Split-Path $csprojFilePath;
|
||||
$signKeyFile = Join-Path $directory "signKey.snk";
|
||||
|
||||
$bytes = [Convert]::FromBase64String($signingKey)
|
||||
[IO.File]::WriteAllBytes($signKeyFile, $bytes)
|
||||
|
||||
dotnet build $csprojFilePath -c Release -p:ContinuousIntegrationBuild=true -p:DelaySign=false -p:AssemblyOriginatorKeyFile=$signKeyFile
|
|
@ -8,7 +8,7 @@ As with any library/package there are public interfaces with which we interact w
|
|||
The rules used in this system is mostly comprising of [lambda expressions](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions). Anything that can be defined in a lambda expression can be used as a rule in this library.
|
||||
|
||||
#### Rules Schema
|
||||
Rules schema is available in the [schema file](https://github.com/microsoft/RulesEngine/blob/master/schema/workflowRules-schema.json). The workflow rules are how we store the rules in the system. In our system, the name of the model typed in the library is [WorkflowRules](https://github.com/microsoft/RulesEngine/blob/master/src/RulesEngine/RulesEngine/Models/WorkflowRules.cs). An example json would be –
|
||||
Rules schema is available in the [schema file](https://github.com/microsoft/RulesEngine/blob/main/schema/workflow-schema.json). The workflow rules are how we store the rules in the system. In our system, the name of the model typed in the library is [Workflow](https://github.com/microsoft/RulesEngine/blob/main/src/RulesEngine/Models/Workflow.cs). An example json would be –
|
||||
|
||||
```json
|
||||
[
|
||||
|
@ -61,7 +61,7 @@ Rules schema is available in the [schema file](https://github.com/microsoft/Rule
|
|||
```
|
||||
|
||||
This workflow rules showcased in the above json is of a sample [Use Case](https://github.com/microsoft/RulesEngine/wiki/Use-Case) which is going to be used to explain the library.
|
||||
Demo App for the given use case is available at [this location](https://github.com/microsoft/RulesEngine/tree/master/demo).
|
||||
Demo App for the given use case is available at [this location](https://github.com/microsoft/RulesEngine/tree/main/demo).
|
||||
#### Logger
|
||||
Another public interface for custom logging is ILogger. This interface is not implemented and looks for custom implementation of the user who wants to use it. The methods available for this interface are –
|
||||
```c#
|
||||
|
@ -112,7 +112,7 @@ new RulesEngine.RulesEngine(workflowRules.ToArray(), null, reSettingsWithCustomT
|
|||
```
|
||||
|
||||
#### RuleParameter
|
||||
This is a model class for custom inputs which can be seen in the [RuleParameter Class](https://github.com/microsoft/RulesEngine/blob/master/src/RulesEngine/RulesEngine/Models/RuleParameter.cs). This type is present to add another layer of customization to the rules.
|
||||
This is a model class for custom inputs which can be seen in the [RuleParameter Class](https://github.com/microsoft/RulesEngine/blob/main/src/RulesEngine/Models/RuleParameter.cs). This type is present to add another layer of customization to the rules.
|
||||
|
||||
For example, the rules present in the example mentioned in the [Rules Schema](#rules-schema) section are using 3 different inputs for each run. The inputs are of different types as mentioned in the [Use Case]((https://github.com/microsoft/RulesEngine/wiki/Use-Case)) and is coming from different sources. Now, in rules we had to use input1, input2 and input3 to target data coming from the basic info, order info and telemetry info, respectively.
|
||||
|
||||
|
@ -150,7 +150,7 @@ Below is an example of a complex rule which can be authored easily using logical
|
|||
|
||||
|
||||
#### RuleResultTree
|
||||
[This model](https://github.com/microsoft/RulesEngine/blob/master/src/RulesEngine/RulesEngine/Models/RuleResultTree.cs) is the output of the Rules Engine. Once the execution of the Rules Engine is completed and the Engine has gone through all the rules, a list of this type is returned. What this model include is –
|
||||
[This model](https://github.com/microsoft/RulesEngine/blob/main/src/RulesEngine/Models/RuleResultTree.cs) is the output of the Rules Engine. Once the execution of the Rules Engine is completed and the Engine has gone through all the rules, a list of this type is returned. What this model include is –
|
||||
##### Rule
|
||||
This is the rule that is currently being referred. It is of a custom model type and has information of that rule which ran on the input.
|
||||
##### IsSuccess
|
||||
|
|
|
@ -8,7 +8,7 @@ These all features make this library highly configurable and extensible as shown
|
|||
|
||||
### How it works
|
||||
|
||||
[[https://github.com/microsoft/RulesEngine/blob/master/assets/BlockDiagram.png|alt=octocat]]
|
||||
![](https://github.com/microsoft/RulesEngine/blob/main/assets/BlockDiagram.png)
|
||||
|
||||
Here. there are multiple actors/component involved.
|
||||
##### Rules Engine
|
||||
|
|
455
docs/index.md
455
docs/index.md
|
@ -1,14 +1,44 @@
|
|||
## Description
|
||||
RulesEngine is a highly extensible library to build rule based system using C# expressions
|
||||
|
||||
## Features
|
||||
- Json based rules defination
|
||||
|
||||
**Features**
|
||||
- Json based rules definition
|
||||
- Multiple input support
|
||||
- Dynamic object input support
|
||||
- C# Expression support
|
||||
- Extending expression via custom class/type injection
|
||||
- Scoped parameters
|
||||
- Post rule execution actions
|
||||
- Standalone expression evaluator
|
||||
|
||||
**Table Of Content**
|
||||
- [Installation](#installation)
|
||||
- [Basic Usage](#basic-usage)
|
||||
- [Create a workflow file with rules](#create-a-workflow-file-with-rules)
|
||||
- [Initialise RulesEngine with the workflow:](#initialise-rulesengine-with-the-workflow)
|
||||
- [Execute the workflow rules with input:](#execute-the-workflow-rules-with-input)
|
||||
- [Using custom names for inputs](#using-custom-names-for-inputs)
|
||||
- [C# Expression support](#c-expression-support)
|
||||
- [Extending expression via custom class/type injection](#extending-expression-via-custom-classtype-injection)
|
||||
- [Example](#example)
|
||||
- [ScopedParams](#scopedparams)
|
||||
- [GlobalParams](#globalparams)
|
||||
- [Example](#example-1)
|
||||
- [LocalParams](#localparams)
|
||||
- [Example](#example-2)
|
||||
- [Referencing ScopedParams in other ScopedParams](#referencing-scopedparams-in-other-scopedparams)
|
||||
- [Post rule execution actions](#post-rule-execution-actions)
|
||||
- [Inbuilt Actions](#inbuilt-actions)
|
||||
- [OutputExpression](#outputexpression)
|
||||
- [Usage](#usage)
|
||||
- [EvaluateRule](#evaluaterule)
|
||||
- [Usage](#usage-1)
|
||||
- [Custom Actions](#custom-actions)
|
||||
- [Steps to use a custom Action](#steps-to-use-a-custom-action)
|
||||
- [Standalone Expression Evaluator](#standalone-expression-evaluator)
|
||||
- [Usage](#usage-2)
|
||||
- [Settings](#settings)
|
||||
- [NestedRuleExecutionMode](#nestedruleexecutionmode)
|
||||
|
||||
|
||||
|
||||
|
@ -38,7 +68,7 @@ Nuget package: [![nuget](https://img.shields.io/nuget/dt/RulesEngine)](https://w
|
|||
### Initialise RulesEngine with the workflow:
|
||||
```c#
|
||||
var workflowRules = //Get list of workflow rules declared in the json
|
||||
var re = new RulesEngine.RulesEngine(workflowRules, null);
|
||||
var re = new RulesEngine.RulesEngine(workflowRules);
|
||||
```
|
||||
|
||||
### Execute the workflow rules with input:
|
||||
|
@ -79,7 +109,7 @@ It is possible to use a custom name in rules by passing input as `RuleParameter`
|
|||
Now we can call rulesEngine with the custom names:
|
||||
```c#
|
||||
var workflowRules = //Get list of workflow rules declared in the json
|
||||
var re = new RulesEngine.RulesEngine(workflowRules, null);
|
||||
var re = new RulesEngine.RulesEngine(workflowRules);
|
||||
|
||||
|
||||
// Declare input1,input2,input3
|
||||
|
@ -92,6 +122,66 @@ var resultList = await re.ExecuteAllRulesAsync("DiscountWithCustomInputNames",r
|
|||
|
||||
```
|
||||
|
||||
## C# Expression support
|
||||
The lambda expression allows you to use most of C# constructs and along with some of linq features.
|
||||
|
||||
For more details on supported expression language refer - [expression language](https://dynamic-linq.net/expression-language)
|
||||
|
||||
For supported linq operations refer - [sequence operators](https://dynamic-linq.net/expression-language#sequence-operators)
|
||||
|
||||
|
||||
## Extending expression via custom class/type injection
|
||||
Although RulesEngine supports C# expressions, you may need to perform more complex operation.
|
||||
|
||||
RulesEngine supports injecting custom classes/types via `ReSettings` which can allow you to call properties and methods of your custom class in expressions
|
||||
|
||||
### Example
|
||||
Create a custom static class
|
||||
```c#
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace RE.HelperFunctions
|
||||
{
|
||||
public static class Utils
|
||||
{
|
||||
public static bool CheckContains(string check, string valList)
|
||||
{
|
||||
if (String.IsNullOrEmpty(check) || String.IsNullOrEmpty(valList))
|
||||
return false;
|
||||
|
||||
var list = valList.Split(',').ToList();
|
||||
return list.Contains(check);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Add it in your ReSettings and pass in RulesEngine constructor
|
||||
|
||||
```c#
|
||||
var reSettings = new ReSettings{
|
||||
CustomTypes = new Type[] { typeof(Utils) }
|
||||
}
|
||||
|
||||
var rulesEngine = new RulesEngine.RulesEngine(workflowRules,reSettings);
|
||||
```
|
||||
|
||||
With this you can call Utils class in your Rules
|
||||
|
||||
```json
|
||||
{
|
||||
"WorkflowName": "DiscountWithCustomInputNames",
|
||||
"Rules": [
|
||||
{
|
||||
"RuleName": "GiveDiscount10",
|
||||
"Expression": "Utils.CheckContains(input1.country, \"india,usa,canada,France\") == true"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## ScopedParams
|
||||
Sometimes Rules can get very long and complex, scopedParams allow users to replace an expression in rule with an alias making it easier to maintain rule.
|
||||
|
@ -106,7 +196,7 @@ GlobalParams are defined at workflow level and can be used in any rule.
|
|||
|
||||
#### Example
|
||||
|
||||
```json
|
||||
```jsonc
|
||||
//Rule.json
|
||||
{
|
||||
"WorkflowName": "workflowWithGlobalParam",
|
||||
|
@ -141,3 +231,356 @@ These rules when executed with the below input will return success
|
|||
```
|
||||
|
||||
|
||||
### LocalParams
|
||||
LocalParams are defined at rule level and can be used by the rule and its child rules
|
||||
|
||||
#### Example
|
||||
|
||||
```jsonc
|
||||
//Rule.json
|
||||
{
|
||||
"WorkflowName": "workflowWithLocalParam",
|
||||
|
||||
"Rules":[
|
||||
{
|
||||
"RuleName": "checkLocalEqualsHello",
|
||||
"LocalParams":[
|
||||
{
|
||||
"Name":"mylocal1",
|
||||
"Expression":"myInput.hello.ToLower()"
|
||||
}
|
||||
],
|
||||
"Expression":"mylocal1 == \"hello\""
|
||||
},
|
||||
{
|
||||
"RuleName": "checkLocalEqualsInputHelloInNested",
|
||||
"LocalParams":[
|
||||
{
|
||||
"Name":"mylocal1", //redefined here as it is scoped at rule level
|
||||
"Expression":"myInput.hello.ToLower()"
|
||||
}
|
||||
],
|
||||
"Operator": "And",
|
||||
"Rules":[
|
||||
{
|
||||
"RuleName": "nestedRule",
|
||||
"Expression":"myInput.hello.ToLower() == mylocal1" //mylocal1 can be used here since it is nested to Rule where mylocal1 is defined
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
These rules when executed with the below input will return success
|
||||
```c#
|
||||
var input = new RuleParameter("myInput",new {
|
||||
hello = "HELLO"
|
||||
});
|
||||
|
||||
var resultList = await re.ExecuteAllRulesAsync("workflowWithLocalParam",rp);
|
||||
```
|
||||
|
||||
### Referencing ScopedParams in other ScopedParams
|
||||
|
||||
Similar to how ScopedParams can be used in expressions, they can also be used in other scoped params that come after them.
|
||||
This allows us to create multi-step rule which is easier to read and maintain
|
||||
|
||||
|
||||
```jsonc
|
||||
//Rule.json
|
||||
{
|
||||
"WorkflowName": "workflowWithReferencedRule",
|
||||
"GlobalParams":[
|
||||
{
|
||||
"Name":"myglobal1",
|
||||
"Expression":"myInput.hello"
|
||||
}
|
||||
],
|
||||
"Rules":[
|
||||
{
|
||||
"RuleName": "checkGlobalAndLocalEqualsHello",
|
||||
"LocalParams":[
|
||||
{
|
||||
"Name": "mylocal1",
|
||||
"Expression": "myglobal1.ToLower()"
|
||||
}
|
||||
],
|
||||
"Expression":"mylocal1 == \"hello\""
|
||||
},
|
||||
{
|
||||
"RuleName": "checklocalEqualsInputHello",
|
||||
"LocalParams":[
|
||||
{
|
||||
"Name": "mylocal1",
|
||||
"Expression": "myglobal1.ToLower()"
|
||||
},
|
||||
{
|
||||
"Name": "mylocal2",
|
||||
"Expression": "myInput.hello.ToLower() == mylocal1"
|
||||
}
|
||||
],
|
||||
"Expression":"mylocal2 == true"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
These rules when executed with the below input will return success
|
||||
```c#
|
||||
var input = new RuleParameter("myInput",new {
|
||||
hello = "HELLO"
|
||||
});
|
||||
|
||||
var resultList = await re.ExecuteAllRulesAsync("workflowWithReferencedRule",rp);
|
||||
|
||||
|
||||
```
|
||||
|
||||
## Post rule execution actions
|
||||
As a part of v3, Actions have been introduced to allow custom code execution on rule result. This can be achieved by calling `ExecuteAllRulesAsync` method of RulesEngine
|
||||
|
||||
### Inbuilt Actions
|
||||
RulesEngine provides inbuilt action which cover major scenarios related to rule execution
|
||||
|
||||
#### OutputExpression
|
||||
This action evaluates an expression based on the RuleParameters and returns its value as Output
|
||||
##### Usage
|
||||
Define OnSuccess or OnFailure Action for your Rule:
|
||||
```jsonc
|
||||
{
|
||||
"WorkflowName": "inputWorkflow",
|
||||
"Rules": [
|
||||
{
|
||||
"RuleName": "GiveDiscount10Percent",
|
||||
"SuccessEvent": "10",
|
||||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.couy == \"india\" AND input1.loyalityFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input2.noOfVisitsPerMonth > 2",
|
||||
"Actions": {
|
||||
"OnSuccess": {
|
||||
"Name": "OutputExpression", //Name of action you want to call
|
||||
"Context": { //This is passed to the action as action context
|
||||
"Expression": "input1.TotalBilled * 0.9"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Call `ExecuteAllRulesAsync` with the workflowName, ruleName and ruleParameters
|
||||
```c#
|
||||
var ruleResultList = await rulesEngine.ExecuteAllRulesAsync("inputWorkflow",ruleParameters);
|
||||
foreach(var ruleResult in ruleResultList){
|
||||
if(ruleResult.ActionResult != null){
|
||||
Console.WriteLine(ruleResult.ActionResult.Output); //ActionResult.Output contains the evaluated value of the action
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
#### EvaluateRule
|
||||
This action allows chaining of rules along with their actions. It also supports filtering inputs provided to chained rule as well as providing custom inputs
|
||||
|
||||
##### Usage
|
||||
Define OnSuccess or OnFailure Action for your Rule:
|
||||
```jsonc
|
||||
{
|
||||
"WorkflowName": "inputWorkflow",
|
||||
"Rules": [
|
||||
{
|
||||
"RuleName": "GiveDiscount20Percent",
|
||||
"Expression": "input1.couy == \"india\" AND input1.loyalityFactor <= 5 AND input1.totalPurchasesToDate >= 20000",
|
||||
"Actions": {
|
||||
"OnSuccess": {
|
||||
"Name": "OutputExpression", //Name of action you want to call
|
||||
"Context": { //This is passed to the action as action context
|
||||
"Expression": "input1.TotalBilled * 0.8"
|
||||
}
|
||||
},
|
||||
"OnFailure": { // This will execute if the Rule evaluates to failure
|
||||
"Name": "EvaluateRule",
|
||||
"Context": {
|
||||
"WorkflowName": "inputWorkflow",
|
||||
"ruleName": "GiveDiscount10Percent"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"RuleName": "GiveDiscount10Percent",
|
||||
"SuccessEvent": "10",
|
||||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.couy == \"india\" AND input1.loyalityFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input2.noOfVisitsPerMonth > 2",
|
||||
"Actions": {
|
||||
"OnSuccess": {
|
||||
"Name": "OutputExpression", //Name of action you want to call
|
||||
"Context": { //This is passed to the action as action context
|
||||
"Expression": "input1.TotalBilled * 0.9"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Call `ExecuteActionWorkflowAsync` with the workflowName, ruleName and ruleParameters
|
||||
```c#
|
||||
var result = await rulesEngine.ExecuteActionWorkflowAsync("inputWorkflow","GiveDiscount20Percent",ruleParameters);
|
||||
Console.WriteLine(result.Output); //result.Output contains the evaluated value of the action
|
||||
```
|
||||
|
||||
In the above scenario if `GiveDiscount20Percent` succeeds, it will return 20 percent discount in output. If it fails, `EvaluateRule` action will call `GiveDiscount10Percent` internally and if it succeeds, it will return 10 percent discount in output.
|
||||
|
||||
EvaluateRule also supports passing filtered inputs and computed inputs to chained rule
|
||||
```jsonc
|
||||
"Actions": {
|
||||
"OnSuccess": {
|
||||
"Name": "EvaluateRule",
|
||||
"Context": {
|
||||
"WorkflowName": "inputWorkflow",
|
||||
"ruleName": "GiveDiscount10Percent",
|
||||
"inputFilter": ["input2"], //will only pass input2 from existing inputs,scopedparams to the chained rule
|
||||
"additionalInputs":[ // will pass a new input named currentDiscount with the result of the expression to the chained rule
|
||||
{
|
||||
"Name": "currentDiscount",
|
||||
"Expression": "input1.TotalBilled * 0.9"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
### Custom Actions
|
||||
RulesEngine allows registering custom actions which can be used in the rules workflow.
|
||||
|
||||
#### Steps to use a custom Action
|
||||
1. Create a class which extends `ActionBase` class and implement the run method
|
||||
```c#
|
||||
public class MyCustomAction: ActionBase
|
||||
{
|
||||
|
||||
public MyCustomAction(SomeInput someInput)
|
||||
{
|
||||
....
|
||||
}
|
||||
|
||||
public override ValueTask<object> Run(ActionContext context, RuleParameter[] ruleParameters)
|
||||
{
|
||||
var customInput = context.GetContext<string>("customContextInput");
|
||||
//Add your custom logic here and return a ValueTask
|
||||
}
|
||||
```
|
||||
Actions can have async code as well
|
||||
```c#
|
||||
public class MyCustomAction: ActionBase
|
||||
{
|
||||
|
||||
public MyCustomAction(SomeInput someInput)
|
||||
{
|
||||
....
|
||||
}
|
||||
|
||||
public override async ValueTask<object> Run(ActionContext context, RuleParameter[] ruleParameters)
|
||||
{
|
||||
var customInput = context.GetContext<string>("customContextInput");
|
||||
//Add your custom logic here
|
||||
return await MyCustomLogicAsync();
|
||||
}
|
||||
```
|
||||
2. Register them in ReSettings and pass it to RulesEngine
|
||||
```c#
|
||||
var reSettings = new ReSettings{
|
||||
CustomActions = new Dictionary<string, Func<ActionBase>>{
|
||||
{"MyCustomAction", () => new MyCustomAction(someInput) }
|
||||
}
|
||||
};
|
||||
|
||||
var re = new RulesEngine(workflowRules,reSettings);
|
||||
```
|
||||
3. You can now use the name you registered in the Rules json in success or failure actions
|
||||
```jsonc
|
||||
{
|
||||
"WorkflowName": "inputWorkflow",
|
||||
"Rules": [
|
||||
{
|
||||
"RuleName": "GiveDiscount10Percent",
|
||||
"SuccessEvent": "10",
|
||||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.couy == \"india\" AND input1.loyalityFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input2.noOfVisitsPerMonth > 2",
|
||||
"Actions": {
|
||||
"OnSuccess": {
|
||||
"Name": "MyCustomAction", //Name context
|
||||
"Context": { //This is passed to the action as action context
|
||||
"customContextInput": "input1.TotalBilled * 0.9"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Standalone Expression Evaluator
|
||||
If you are not looking for a full fledged RulesEngine and need only an expression evaluator. RulesEngine offers `RuleExpressionParser` which handles expression parsing and evaluation.
|
||||
|
||||
### Usage
|
||||
```c#
|
||||
using System;
|
||||
using RulesEngine.Models;
|
||||
using RulesEngine.ExpressionBuilders;
|
||||
|
||||
public class Program
|
||||
{
|
||||
public static void Main()
|
||||
{
|
||||
var reParser = new RuleExpressionParser(new ReSettings());
|
||||
var result = reParser.Evaluate<string>("a+b", new RuleParameter[]{
|
||||
new RuleParameter("a","Hello "),
|
||||
new RuleParameter("b","World")
|
||||
});
|
||||
Console.WriteLine(result);
|
||||
}
|
||||
}
|
||||
```
|
||||
This will output "Hello World"
|
||||
|
||||
For more advanced usage, refer - https://dotnetfiddle.net/KSX8i0
|
||||
|
||||
## Settings
|
||||
RulesEngine allows you to pass optional `ReSettings` in constructor to specify certain configuration for RulesEngine.
|
||||
|
||||
Here are the all the options available:-
|
||||
|
||||
|
||||
| Property | Type | Default Value | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `CustomTypes` | `Type[]` | N/A | Custom types to be used in rule expressions. |
|
||||
| `CustomActions` | `Dictionary<string, Func<ActionBase>>` | N/A | Custom actions that can be used in the rules. |
|
||||
| `EnableExceptionAsErrorMessage` | `bool` | `true` | If `true`, returns any exception occurred while rule execution as an error message. Otherwise, throws an exception. This setting is only applicable if `IgnoreException` is set to `false`. |
|
||||
| `IgnoreException` | `bool` | `false` | If `true`, it will ignore any exception thrown with rule compilation/execution. |
|
||||
| `EnableFormattedErrorMessage` | `bool` | `true` | Enables error message formatting. |
|
||||
| `EnableScopedParams` | `bool` | `true` | Enables global parameters and local parameters for rules. |
|
||||
| `IsExpressionCaseSensitive` | `bool` | `false` | Sets whether expressions are case sensitive. |
|
||||
| `AutoRegisterInputType` | `bool` | `true` | Auto registers input type in custom type to allow calling method on type. |
|
||||
| `NestedRuleExecutionMode` | `NestedRuleExecutionMode` | `All` | Sets the mode for nested rule execution. |
|
||||
| `CacheConfig` | `MemCacheConfig` | N/A | Configures the memory cache. |
|
||||
| `UseFastExpressionCompiler` | `bool` | `true` | Whether to use FastExpressionCompiler for rule compilation. |
|
||||
|
||||
|
||||
### NestedRuleExecutionMode
|
||||
|
||||
| Value | Description |
|
||||
| --- | --- |
|
||||
| `All` | Executes all nested rules. |
|
||||
| `Performance` | Skips nested rules whose execution does not impact parent rule's result. |
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"sdk": {
|
||||
"version": "3.1",
|
||||
"version": "8.0.0",
|
||||
"rollForward": "latestFeature",
|
||||
"allowPrerelease": false
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -1,12 +1,24 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"ScopedParam": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Name": { "type": "string" },
|
||||
"Expression": { "type": "string" }
|
||||
},
|
||||
"required": [ "Name", "Expression" ]
|
||||
},
|
||||
"Rule": {
|
||||
"title": "Rule",
|
||||
"properties": {
|
||||
"RuleName": {
|
||||
"type": "string"
|
||||
},
|
||||
"LocalParams": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/ScopedParam" }
|
||||
},
|
||||
"Operator": {
|
||||
"enum": [
|
||||
"And",
|
||||
|
@ -18,12 +30,6 @@
|
|||
"ErrorMessage": {
|
||||
"type": "string"
|
||||
},
|
||||
"ErrorType": {
|
||||
"enum": [
|
||||
"Warning",
|
||||
"Error"
|
||||
]
|
||||
},
|
||||
"SuccessEvent": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -39,6 +45,16 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Properties": {
|
||||
"type": "object"
|
||||
},
|
||||
"Actions": {
|
||||
"$ref": "#/definitions/RuleActions"
|
||||
},
|
||||
"Enabled": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
@ -53,13 +69,16 @@
|
|||
"type": "object",
|
||||
"required": [
|
||||
"RuleName",
|
||||
"Expression",
|
||||
"RuleExpressionType"
|
||||
"Expression"
|
||||
],
|
||||
"properties": {
|
||||
"RuleName": {
|
||||
"type": "string"
|
||||
},
|
||||
"LocalParams": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/ScopedParam" }
|
||||
},
|
||||
"Expression": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -71,22 +90,58 @@
|
|||
"ErrorMessage": {
|
||||
"type": "string"
|
||||
},
|
||||
"ErrorType": {
|
||||
"enum": [
|
||||
"Warning",
|
||||
"Error"
|
||||
]
|
||||
},
|
||||
"SuccessEvent": {
|
||||
"type": "string"
|
||||
},
|
||||
"Properties": {
|
||||
"type": "object"
|
||||
},
|
||||
"Actions": {
|
||||
"$ref": "#/definitions/RuleActions"
|
||||
},
|
||||
"Enabled": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"ActionInfo": {
|
||||
"properties": {
|
||||
"Name": {
|
||||
"type": "string"
|
||||
},
|
||||
"Context": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Name"
|
||||
]
|
||||
},
|
||||
"RuleActions": {
|
||||
"properties": {
|
||||
"OnSuccess": {
|
||||
"$ref": "#/definitions/ActionInfo"
|
||||
},
|
||||
"OnFailure": {
|
||||
"$ref": "#/definitions/ActionInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
"properties": {
|
||||
"WorkFlowName": {
|
||||
"WorkflowName": {
|
||||
"type": "string"
|
||||
},
|
||||
"WorkflowsToInject": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"GlobalParams": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/ScopedParam" }
|
||||
},
|
||||
"Rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
@ -102,7 +157,7 @@
|
|||
}
|
||||
},
|
||||
"required": [
|
||||
"WorkFlowName",
|
||||
"WorkflowName",
|
||||
"Rules"
|
||||
],
|
||||
"type": "object"
|
|
@ -0,0 +1,16 @@
|
|||
param(
|
||||
[Parameter(Mandatory=$true)][string] $reportPath,
|
||||
[Parameter(Mandatory=$true)][decimal] $threshold
|
||||
)
|
||||
|
||||
|
||||
[XML]$report = Get-Content $reportPath;
|
||||
[decimal]$coverage = [decimal]$report.coverage.'line-rate' * 100;
|
||||
|
||||
if ($coverage -lt $threshold) {
|
||||
Write-Error "Coverage($coverage) is less than $threshold percent"
|
||||
exit 1
|
||||
}
|
||||
else{
|
||||
Write-Host "Coverage($coverage) is more than $threshold percent"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
dotnet tool restore
|
||||
dotnet reportgenerator "-reports:**/coverage.cobertura.xml" "-targetdir:coveragereport" -reporttypes:"Html;lcov;Cobertura"
|
Binary file not shown.
|
@ -19,7 +19,18 @@ namespace RulesEngine.Actions
|
|||
foreach (var kv in context)
|
||||
{
|
||||
string key = kv.Key;
|
||||
string value = kv.Value is string ? kv.Value.ToString() : JsonConvert.SerializeObject(kv.Value);
|
||||
string value;
|
||||
switch (kv.Value.GetType().Name)
|
||||
{
|
||||
case "String":
|
||||
case "JsonElement":
|
||||
value = kv.Value.ToString();
|
||||
break;
|
||||
default:
|
||||
value = JsonConvert.SerializeObject(kv.Value);
|
||||
break;
|
||||
|
||||
}
|
||||
_context.Add(key, value);
|
||||
}
|
||||
_parentResult = parentResult;
|
||||
|
@ -29,6 +40,21 @@ namespace RulesEngine.Actions
|
|||
{
|
||||
return _parentResult;
|
||||
}
|
||||
|
||||
public bool TryGetContext<T>(string name,out T output)
|
||||
{
|
||||
try
|
||||
{
|
||||
output = GetContext<T>(name);
|
||||
return true;
|
||||
}
|
||||
catch(ArgumentException)
|
||||
{
|
||||
output = default(T);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public T GetContext<T>(string name)
|
||||
{
|
||||
try
|
||||
|
|
|
@ -13,7 +13,6 @@ namespace RulesEngine.Actions
|
|||
internal ActionFactory()
|
||||
{
|
||||
_actionRegistry = new Dictionary<string, Func<ActionBase>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
}
|
||||
internal ActionFactory(IDictionary<string, Func<ActionBase>> actionRegistry) : this()
|
||||
{
|
||||
|
@ -29,7 +28,7 @@ namespace RulesEngine.Actions
|
|||
{
|
||||
return _actionRegistry[name]();
|
||||
}
|
||||
throw new KeyNotFoundException($"Action with name:{name} does not exist");
|
||||
throw new KeyNotFoundException($"Action with name: {name} does not exist");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using RulesEngine.ExpressionBuilders;
|
||||
using RulesEngine.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RulesEngine.Actions
|
||||
|
@ -10,10 +13,12 @@ namespace RulesEngine.Actions
|
|||
public class EvaluateRuleAction : ActionBase
|
||||
{
|
||||
private readonly RulesEngine _ruleEngine;
|
||||
private readonly RuleExpressionParser _ruleExpressionParser;
|
||||
|
||||
public EvaluateRuleAction(RulesEngine ruleEngine)
|
||||
public EvaluateRuleAction(RulesEngine ruleEngine, RuleExpressionParser ruleExpressionParser)
|
||||
{
|
||||
_ruleEngine = ruleEngine;
|
||||
_ruleExpressionParser = ruleExpressionParser;
|
||||
}
|
||||
|
||||
internal async override ValueTask<ActionRuleResult> ExecuteAndReturnResultAsync(ActionContext context, RuleParameter[] ruleParameters, bool includeRuleResults = false)
|
||||
|
@ -23,11 +28,11 @@ namespace RulesEngine.Actions
|
|||
List<RuleResultTree> resultList = null;
|
||||
if (includeRuleResults)
|
||||
{
|
||||
resultList = new List<RuleResultTree>(output.Results);
|
||||
resultList = new List<RuleResultTree>(output?.Results ?? new List<RuleResultTree>() { });
|
||||
resultList.AddRange(innerResult.Results);
|
||||
}
|
||||
return new ActionRuleResult {
|
||||
Output = output.Output,
|
||||
Output = output?.Output,
|
||||
Exception = innerResult.Exception,
|
||||
Results = resultList
|
||||
};
|
||||
|
@ -37,7 +42,22 @@ namespace RulesEngine.Actions
|
|||
{
|
||||
var workflowName = context.GetContext<string>("workflowName");
|
||||
var ruleName = context.GetContext<string>("ruleName");
|
||||
var ruleResult = await _ruleEngine.ExecuteActionWorkflowAsync(workflowName, ruleName, ruleParameters);
|
||||
var filteredRuleParameters = new List<RuleParameter>(ruleParameters);
|
||||
if(context.TryGetContext<List<string>>("inputFilter",out var inputFilter))
|
||||
{
|
||||
filteredRuleParameters = ruleParameters.Where(c => inputFilter.Contains(c.Name)).ToList();
|
||||
}
|
||||
if (context.TryGetContext<List<ScopedParam>>("additionalInputs", out var additionalInputs))
|
||||
{
|
||||
foreach(var additionalInput in additionalInputs)
|
||||
{
|
||||
dynamic value = _ruleExpressionParser.Evaluate<object>(additionalInput.Expression, ruleParameters);
|
||||
filteredRuleParameters.Add(new RuleParameter(additionalInput.Name, value));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var ruleResult = await _ruleEngine.ExecuteActionWorkflowAsync(workflowName, ruleName, filteredRuleParameters.ToArray());
|
||||
return ruleResult;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
namespace RulesEngine.Enums
|
||||
{
|
||||
public enum ActionTriggerType
|
||||
{
|
||||
onSuccess,
|
||||
onFailure
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -22,7 +22,7 @@ namespace RulesEngine.ExpressionBuilders
|
|||
/// <returns>Expression type</returns>
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -2,12 +2,13 @@
|
|||
// Licensed under the MIT License.
|
||||
|
||||
using FastExpressionCompiler;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using RulesEngine.HelperFunctions;
|
||||
using RulesEngine.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Dynamic.Core;
|
||||
using System.Linq.Dynamic.Core.Parser;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
|
||||
|
@ -16,15 +17,11 @@ namespace RulesEngine.ExpressionBuilders
|
|||
public class RuleExpressionParser
|
||||
{
|
||||
private readonly ReSettings _reSettings;
|
||||
private static IMemoryCache _memoryCache;
|
||||
private readonly IDictionary<string, MethodInfo> _methodInfo;
|
||||
|
||||
public RuleExpressionParser(ReSettings reSettings)
|
||||
public RuleExpressionParser(ReSettings reSettings = null)
|
||||
{
|
||||
_reSettings = reSettings;
|
||||
_memoryCache = _memoryCache ?? new MemoryCache(new MemoryCacheOptions {
|
||||
SizeLimit = 1000
|
||||
});
|
||||
_reSettings = reSettings ?? new ReSettings();
|
||||
_methodInfo = new Dictionary<string, MethodInfo>();
|
||||
PopulateMethodInfo();
|
||||
}
|
||||
|
@ -34,25 +31,43 @@ namespace RulesEngine.ExpressionBuilders
|
|||
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);
|
||||
}
|
||||
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),
|
||||
IsCaseSensitive = _reSettings.IsExpressionCaseSensitive
|
||||
};
|
||||
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)
|
||||
{
|
||||
var cacheKey = GetCacheKey(expression, ruleParams, typeof(T));
|
||||
return _memoryCache.GetOrCreate(cacheKey, (entry) => {
|
||||
entry.SetSize(1);
|
||||
var parameterExpressions = GetParameterExpression(ruleParams).ToArray();
|
||||
{
|
||||
var rtype = typeof(T);
|
||||
if(rtype == typeof(object))
|
||||
{
|
||||
rtype = null;
|
||||
}
|
||||
var parameterExpressions = GetParameterExpression(ruleParams).ToArray();
|
||||
|
||||
var e = Parse(expression, parameterExpressions, typeof(T));
|
||||
var expressionBody = new List<Expression>() { e.Body };
|
||||
var wrappedExpression = WrapExpression<T>(expressionBody, parameterExpressions, new ParameterExpression[] { });
|
||||
return wrappedExpression.CompileFast();
|
||||
});
|
||||
var e = Parse(expression, parameterExpressions, rtype);
|
||||
if(rtype == null)
|
||||
{
|
||||
e = Expression.Convert(e, typeof(T));
|
||||
}
|
||||
var expressionBody = new List<Expression>() { e };
|
||||
var wrappedExpression = WrapExpression<T>(expressionBody, parameterExpressions, new ParameterExpression[] { });
|
||||
return CompileExpression(wrappedExpression);
|
||||
|
||||
}
|
||||
|
||||
private Func<object[], T> CompileExpression<T>(Expression<Func<object[], T>> expression)
|
||||
{
|
||||
if(_reSettings.UseFastExpressionCompiler)
|
||||
{
|
||||
return expression.CompileFast();
|
||||
}
|
||||
return expression.Compile();
|
||||
}
|
||||
|
||||
private Expression<Func<object[], T>> WrapExpression<T>(List<Expression> expressionList, ParameterExpression[] parameters, ParameterExpression[] variables)
|
||||
|
@ -71,11 +86,11 @@ namespace RulesEngine.ExpressionBuilders
|
|||
{
|
||||
ruleExpParams = ruleExpParams ?? new RuleExpressionParameter[] { };
|
||||
var expression = CreateDictionaryExpression(ruleParams, ruleExpParams);
|
||||
return expression.CompileFast();
|
||||
return CompileExpression(expression);
|
||||
}
|
||||
|
||||
public T Evaluate<T>(string expression, RuleParameter[] ruleParams)
|
||||
{
|
||||
{
|
||||
var func = Compile<T>(expression, ruleParams);
|
||||
return func(ruleParams.Select(c => c.Value).ToArray());
|
||||
}
|
||||
|
@ -144,13 +159,5 @@ namespace RulesEngine.ExpressionBuilders
|
|||
|
||||
return WrapExpression<Dictionary<string,object>>(body, paramExp.ToArray(), variableExp.ToArray());
|
||||
}
|
||||
|
||||
private string GetCacheKey(string expression, RuleParameter[] ruleParameters, Type returnType)
|
||||
{
|
||||
var paramKey = string.Join("|", ruleParameters.Select(c => c.Name + "_" + c.Type.ToString()));
|
||||
var returnTypeKey = returnType?.ToString() ?? "null";
|
||||
var combined = $"Expression:{expression}-Params:{paramKey}-ReturnType:{returnTypeKey}";
|
||||
return combined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RulesEngine.Extensions
|
||||
{
|
||||
internal static class EnumerableExtensions
|
||||
{
|
||||
public static IEnumerable<T> Safe<T>(this IEnumerable<T> enumerable)
|
||||
{
|
||||
return enumerable ?? Enumerable.Empty<T>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ namespace RulesEngine.Extensions
|
|||
|
||||
|
||||
/// <summary>
|
||||
/// Calls the Success Func for the first rule which succeeded among the the ruleReults
|
||||
/// Calls the Success Func for the first rule which succeeded among the ruleResults
|
||||
/// </summary>
|
||||
/// <param name="ruleResultTrees"></param>
|
||||
/// <param name="onSuccessFunc"></param>
|
||||
|
@ -36,7 +36,7 @@ namespace RulesEngine.Extensions
|
|||
/// Calls the Failure Func if all rules failed in the ruleReults
|
||||
/// </summary>
|
||||
/// <param name="ruleResultTrees"></param>
|
||||
/// <param name="onSuccessFunc"></param>
|
||||
/// <param name="onFailureFunc"></param>
|
||||
/// <returns></returns>
|
||||
public static List<RuleResultTree> OnFail(this List<RuleResultTree> ruleResultTrees, OnFailureFunc onFailureFunc)
|
||||
{
|
||||
|
|
|
@ -9,7 +9,7 @@ namespace RulesEngine.HelperFunctions
|
|||
public static class Constants
|
||||
{
|
||||
public const string WORKFLOW_NAME_NULL_ERRMSG = "Workflow name can not be null or empty";
|
||||
public const string INJECT_WORKFLOW_RULES_ERRMSG = "Atleast one of Rules or WorkflowRulesToInject must be not empty";
|
||||
public const string INJECT_WORKFLOW_RULES_ERRMSG = "Atleast one of Rules or WorkflowsToInject must be not empty";
|
||||
public const string RULE_CATEGORY_CONFIGURED_ERRMSG = "Rule Category should be configured";
|
||||
public const string RULE_NULL_ERRMSG = "Rules can not be null or zero";
|
||||
public const string NESTED_RULE_NULL_ERRMSG = "Nested rules can not be null";
|
||||
|
@ -17,6 +17,7 @@ namespace RulesEngine.HelperFunctions
|
|||
public const string OPERATOR_NULL_ERRMSG = "Operator can not be null";
|
||||
public const string OPERATOR_INCORRECT_ERRMSG = "Operator {PropertyValue} is not allowed";
|
||||
public const string RULE_NAME_NULL_ERRMSG = "Rule Name can not be null";
|
||||
public const string OPERATOR_RULES_ERRMSG = "Cannot use Rules field when Operator is null";
|
||||
public const string LAMBDA_EXPRESSION_EXPRESSION_NULL_ERRMSG = "Expression cannot be null or empty when RuleExpressionType is LambdaExpression";
|
||||
public const string LAMBDA_EXPRESSION_OPERATOR_ERRMSG = "Cannot use Operator field when RuleExpressionType is LambdaExpression";
|
||||
public const string LAMBDA_EXPRESSION_RULES_ERRMSG = "Cannot use Rules field when RuleExpressionType is LambdaExpression";
|
||||
|
|
|
@ -10,7 +10,7 @@ namespace RulesEngine.HelperFunctions
|
|||
{
|
||||
public static bool CheckContains(string check, string valList)
|
||||
{
|
||||
if (String.IsNullOrEmpty(check) || String.IsNullOrEmpty(valList))
|
||||
if (string.IsNullOrEmpty(check) || string.IsNullOrEmpty(valList))
|
||||
return false;
|
||||
|
||||
var list = valList.Split(',').ToList();
|
||||
|
|
|
@ -74,50 +74,5 @@ namespace RulesEngine.HelperFunctions
|
|||
{
|
||||
return reSettings.IgnoreException ? "" : message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// To the result tree error messages
|
||||
/// </summary>
|
||||
/// <param name="ruleResultTree">ruleResultTree</param>
|
||||
/// <param name="ruleResultMessage">ruleResultMessage</param>
|
||||
[Obsolete]
|
||||
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 is 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>
|
||||
[Obsolete]
|
||||
private static void GetChildRuleMessages(IEnumerable<RuleResultTree> childResultTree, ref RuleResultMessage ruleResultMessage)
|
||||
{
|
||||
foreach (var item in childResultTree)
|
||||
{
|
||||
ToResultTreeMessages(item, ref ruleResultMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,8 +29,8 @@ namespace RulesEngine.Interfaces
|
|||
/// <summary>
|
||||
/// Adds new workflows to RulesEngine
|
||||
/// </summary>
|
||||
/// <param name="workflowRules"></param>
|
||||
void AddWorkflow(params WorkflowRules[] workflowRules);
|
||||
/// <param name="workflow"></param>
|
||||
void AddWorkflow(params Workflow[] Workflows);
|
||||
|
||||
/// <summary>
|
||||
/// Removes all registered workflows from RulesEngine
|
||||
|
@ -41,12 +41,20 @@ namespace RulesEngine.Interfaces
|
|||
/// Removes the workflow from RulesEngine
|
||||
/// </summary>
|
||||
/// <param name="workflowNames"></param>
|
||||
void RemoveWorkflow(params string[] workflowNames);
|
||||
void RemoveWorkflow(params string[] workflowNames);
|
||||
|
||||
/// <summary>
|
||||
/// Checks is workflow exist.
|
||||
/// </summary>
|
||||
/// <param name="workflowName">The workflow name.</param>
|
||||
/// <returns> <c>true</c> if contains the specified workflow name; otherwise, <c>false</c>.</returns>
|
||||
bool ContainsWorkflow(string workflowName);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the list of all registered workflow names
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
List<string> GetAllRegisteredWorkflowNames();
|
||||
void AddOrUpdateWorkflow(params Workflow[] Workflows);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the MIT License.
|
||||
|
||||
using RulesEngine.Actions;
|
||||
using RulesEngine.HelperFunctions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
@ -11,6 +12,25 @@ namespace RulesEngine.Models
|
|||
[ExcludeFromCodeCoverage]
|
||||
public class ReSettings
|
||||
{
|
||||
public ReSettings() { }
|
||||
|
||||
// create a copy of settings
|
||||
internal ReSettings(ReSettings reSettings)
|
||||
{
|
||||
CustomTypes = reSettings.CustomTypes;
|
||||
CustomActions = reSettings.CustomActions;
|
||||
EnableExceptionAsErrorMessage = reSettings.EnableExceptionAsErrorMessage;
|
||||
IgnoreException = reSettings.IgnoreException;
|
||||
EnableFormattedErrorMessage = reSettings.EnableFormattedErrorMessage;
|
||||
EnableScopedParams = reSettings.EnableScopedParams;
|
||||
NestedRuleExecutionMode = reSettings.NestedRuleExecutionMode;
|
||||
CacheConfig = reSettings.CacheConfig;
|
||||
IsExpressionCaseSensitive = reSettings.IsExpressionCaseSensitive;
|
||||
AutoRegisterInputType = reSettings.AutoRegisterInputType;
|
||||
UseFastExpressionCompiler = reSettings.UseFastExpressionCompiler;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get/Set the custom types to be used in Rule expressions
|
||||
/// </summary>
|
||||
|
@ -45,12 +65,36 @@ namespace RulesEngine.Models
|
|||
public bool EnableScopedParams { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Enables Local params for rules
|
||||
/// Sets whether expression are case sensitive
|
||||
/// </summary>
|
||||
[Obsolete("Use 'EnableScopedParams' instead. This will be removed in next major version")]
|
||||
public bool EnableLocalParams {
|
||||
get { return EnableScopedParams; }
|
||||
set { EnableScopedParams = value; }
|
||||
}
|
||||
public bool IsExpressionCaseSensitive { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Auto Registers input type in Custom Type to allow calling method on type.
|
||||
/// Default : true
|
||||
/// </summary>
|
||||
public bool AutoRegisterInputType { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the mode for Nested rule execution, Default: All
|
||||
/// </summary>
|
||||
public NestedRuleExecutionMode NestedRuleExecutionMode { get; set; } = NestedRuleExecutionMode.All;
|
||||
public MemCacheConfig CacheConfig { get; set; }
|
||||
/// <summary>
|
||||
/// Whether to use FastExpressionCompiler for rule compilation
|
||||
/// </summary>
|
||||
public bool UseFastExpressionCompiler { get; set; } = true;
|
||||
}
|
||||
|
||||
public enum NestedRuleExecutionMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes all nested rules
|
||||
/// </summary>
|
||||
All,
|
||||
/// <summary>
|
||||
/// Skips nested rules whose execution does not impact parent rule's result
|
||||
/// </summary>
|
||||
Performance
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using RulesEngine.Enums;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
|
@ -35,17 +33,13 @@ namespace RulesEngine.Models
|
|||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
[Obsolete("will be removed in next major version")]
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public ErrorType ErrorType { get; set; } = ErrorType.Warning;
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public RuleExpressionType RuleExpressionType { get; set; } = RuleExpressionType.LambdaExpression;
|
||||
public List<string> WorkflowRulesToInject { get; set; }
|
||||
public List<Rule> Rules { get; set; }
|
||||
public IEnumerable<string> WorkflowsToInject { get; set; }
|
||||
public IEnumerable<Rule> Rules { get; set; }
|
||||
public IEnumerable<ScopedParam> LocalParams { get; set; }
|
||||
public string Expression { get; set; }
|
||||
public Dictionary<ActionTriggerType, ActionInfo> Actions { get; set; }
|
||||
public RuleActions Actions { get; set; }
|
||||
public string SuccessEvent { get; set; }
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace RulesEngine.Models
|
||||
{
|
||||
[Obsolete("RuleAction class is deprecated. Use RuleActions class instead.")]
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class RuleAction : RuleActions
|
||||
{
|
||||
}
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class RuleActions
|
||||
{
|
||||
public ActionInfo OnSuccess { get; set; }
|
||||
public ActionInfo OnFailure { get; set; }
|
||||
}
|
||||
}
|
|
@ -17,10 +17,13 @@ namespace RulesEngine.Models
|
|||
Init(name, Value?.GetType());
|
||||
}
|
||||
|
||||
internal RuleParameter(string name, Type type)
|
||||
|
||||
internal RuleParameter(string name, Type type,object value = null)
|
||||
{
|
||||
Value = Utils.GetTypedObject(value);
|
||||
Init(name, type);
|
||||
}
|
||||
|
||||
public Type Type { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
public object Value { get; private set; }
|
||||
|
@ -33,5 +36,13 @@ namespace RulesEngine.Models
|
|||
ParameterExpression = Expression.Parameter(Type, Name);
|
||||
}
|
||||
|
||||
public static RuleParameter Create<T>(string name, T value)
|
||||
{
|
||||
var typedValue = Utils.GetTypedObject(value);
|
||||
var type = typedValue?.GetType() ?? typeof(T);
|
||||
return new RuleParameter(name,type,value);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,29 +50,6 @@ namespace RulesEngine.Models
|
|||
/// </summary>
|
||||
public string ExceptionMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the rule evaluated parameters.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The rule evaluated parameters.
|
||||
/// </value>
|
||||
[Obsolete("Use `Inputs` field to get details of all input, localParams and globalParams")]
|
||||
public IEnumerable<RuleParameter> RuleEvaluatedParams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This method will return all the error and warning messages to caller
|
||||
/// </summary>
|
||||
/// <returns>RuleResultMessage</returns>
|
||||
[ExcludeFromCodeCoverage]
|
||||
[Obsolete("will be removed in next major version")]
|
||||
public RuleResultMessage GetMessages()
|
||||
{
|
||||
var ruleResultMessage = new RuleResultMessage();
|
||||
|
||||
Helpers.ToResultTreeMessages(this, ref ruleResultMessage);
|
||||
|
||||
return ruleResultMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -82,7 +59,7 @@ namespace RulesEngine.Models
|
|||
public class RuleResultMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor will innitilaze the List
|
||||
/// Constructor will initialize the List
|
||||
/// </summary>
|
||||
public RuleResultMessage()
|
||||
{
|
||||
|
|
|
@ -1,16 +1,24 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace RulesEngine.Models
|
||||
{
|
||||
[Obsolete("WorkflowRules class is deprecated. Use Workflow class instead.")]
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class WorkflowRules : Workflow {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Workflow rules class for deserialization the json config file
|
||||
/// </summary>
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class WorkflowRules
|
||||
public class Workflow
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the workflow name.
|
||||
|
@ -19,7 +27,13 @@ namespace RulesEngine.Models
|
|||
|
||||
/// <summary>Gets or sets the workflow rules to inject.</summary>
|
||||
/// <value>The workflow rules to inject.</value>
|
||||
public IEnumerable<string> WorkflowRulesToInject { get; set; }
|
||||
[Obsolete("WorkflowRulesToInject is deprecated. Use WorkflowsToInject instead.")]
|
||||
public IEnumerable<string> WorkflowRulesToInject {
|
||||
set { WorkflowsToInject = value; }
|
||||
}
|
||||
public IEnumerable<string> WorkflowsToInject { get; set; }
|
||||
|
||||
public RuleExpressionType RuleExpressionType { get; set; } = RuleExpressionType.LambdaExpression;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the global params which will be applicable to all rules
|
|
@ -8,4 +8,4 @@ using System.Runtime.InteropServices;
|
|||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: InternalsVisibleTo("RulesEngine.UnitTest")]
|
||||
[assembly: InternalsVisibleTo("RulesEngine.UnitTest, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c15956b2ac0945c55b69a185f5c3e02276693b0a5e42c8a1f08cb24e03dd87d91f9fa09f79b6b7b3aac4df46f2ea4ce4bfa31920bb0aad9f02793ab29de9fbf40f5ba9e347aa8569128459f31da1f6357eabe6e1308ac7c16b87a4d61e8d1785746a57ec67956d2e2454b3c98502a5d5c4a4168133bfaa431207c108efae03aa")]
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RulesEngine.Exceptions;
|
||||
using RulesEngine.ExpressionBuilders;
|
||||
using RulesEngine.HelperFunctions;
|
||||
|
@ -10,7 +9,6 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace RulesEngine
|
||||
{
|
||||
|
@ -30,20 +28,13 @@ namespace RulesEngine
|
|||
private readonly RuleExpressionBuilderFactory _expressionBuilderFactory;
|
||||
private readonly ReSettings _reSettings;
|
||||
|
||||
/// <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, ReSettings reSettings, ILogger logger)
|
||||
internal RuleCompiler(RuleExpressionBuilderFactory expressionBuilderFactory, ReSettings reSettings)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException($"{nameof(logger)} can't be null.");
|
||||
|
||||
_expressionBuilderFactory = expressionBuilderFactory ?? throw new ArgumentNullException($"{nameof(expressionBuilderFactory)} can't be null.");
|
||||
_reSettings = reSettings;
|
||||
}
|
||||
|
@ -56,26 +47,26 @@ namespace RulesEngine
|
|||
/// <param name="input"></param>
|
||||
/// <param name="ruleParam"></param>
|
||||
/// <returns>Compiled func delegate</returns>
|
||||
internal RuleFunc<RuleResultTree> CompileRule(Rule rule, RuleParameter[] ruleParams, ScopedParam[] globalParams)
|
||||
internal RuleFunc<RuleResultTree> CompileRule(Rule rule, RuleExpressionType ruleExpressionType, RuleParameter[] ruleParams, Lazy<RuleExpressionParameter[]> globalParams)
|
||||
{
|
||||
if (rule == null)
|
||||
{
|
||||
var ex = new ArgumentNullException(nameof(rule));
|
||||
_logger.LogError(ex.Message);
|
||||
throw ex;
|
||||
}
|
||||
try
|
||||
{
|
||||
var globalParamExp = GetRuleExpressionParameters(rule.RuleExpressionType,globalParams, ruleParams);
|
||||
var globalParamExp = globalParams.Value;
|
||||
var extendedRuleParams = ruleParams.Concat(globalParamExp.Select(c => new RuleParameter(c.ParameterExpression.Name,c.ParameterExpression.Type)))
|
||||
.ToArray();
|
||||
var ruleExpression = GetDelegateForRule(rule, extendedRuleParams);
|
||||
|
||||
|
||||
return GetWrappedRuleFunc(rule,ruleExpression,ruleParams,globalParamExp);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var message = $"Error while compiling rule `{rule.RuleName}`: {ex.Message}";
|
||||
_logger.LogError(message);
|
||||
return Helpers.ToRuleExceptionResult(_reSettings, rule, new RuleException(message, ex));
|
||||
}
|
||||
}
|
||||
|
@ -111,7 +102,7 @@ namespace RulesEngine
|
|||
return GetWrappedRuleFunc(rule, ruleFn, ruleParams, scopedParamList);
|
||||
}
|
||||
|
||||
private RuleExpressionParameter[] GetRuleExpressionParameters(RuleExpressionType ruleExpressionType,IEnumerable<ScopedParam> localParams, RuleParameter[] ruleParams)
|
||||
internal RuleExpressionParameter[] GetRuleExpressionParameters(RuleExpressionType ruleExpressionType,IEnumerable<ScopedParam> localParams, RuleParameter[] ruleParams)
|
||||
{
|
||||
if(!_reSettings.EnableScopedParams)
|
||||
{
|
||||
|
@ -131,7 +122,7 @@ namespace RulesEngine
|
|||
{
|
||||
try
|
||||
{
|
||||
var lpExpression = expressionBuilder.Parse(lp.Expression, parameters.ToArray(), null).Body;
|
||||
var lpExpression = expressionBuilder.Parse(lp.Expression, parameters.ToArray(), null);
|
||||
var ruleExpParam = new RuleExpressionParameter() {
|
||||
ParameterExpression = Expression.Parameter(lpExpression.Type, lp.Name),
|
||||
ValueExpression = lpExpression
|
||||
|
@ -186,33 +177,62 @@ namespace RulesEngine
|
|||
}
|
||||
|
||||
return (paramArray) => {
|
||||
var resultList = ruleFuncList.Select(fn => fn(paramArray)).ToList();
|
||||
Func<object[], bool> isSuccess = (p) => ApplyOperation(resultList, operation);
|
||||
var result = Helpers.ToResultTree(_reSettings, parentRule, resultList, isSuccess);
|
||||
var (isSuccess, resultList) = ApplyOperation(paramArray, ruleFuncList, operation);
|
||||
bool isSuccessFn(object[] p) => isSuccess;
|
||||
var result = Helpers.ToResultTree(_reSettings, parentRule, resultList, isSuccessFn);
|
||||
return result(paramArray);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private bool ApplyOperation(IEnumerable<RuleResultTree> ruleResults, ExpressionType operation)
|
||||
private (bool isSuccess ,IEnumerable<RuleResultTree> result) ApplyOperation(RuleParameter[] paramArray,IEnumerable<RuleFunc<RuleResultTree>> ruleFuncList, ExpressionType operation)
|
||||
{
|
||||
if (ruleResults?.Any() != true)
|
||||
if (ruleFuncList?.Any() != true)
|
||||
{
|
||||
return false;
|
||||
return (false,new List<RuleResultTree>());
|
||||
}
|
||||
|
||||
switch (operation)
|
||||
{
|
||||
case ExpressionType.And:
|
||||
case ExpressionType.AndAlso:
|
||||
return ruleResults.All(r => r.IsSuccess);
|
||||
var resultList = new List<RuleResultTree>();
|
||||
var isSuccess = false;
|
||||
|
||||
case ExpressionType.Or:
|
||||
case ExpressionType.OrElse:
|
||||
return ruleResults.Any(r => r.IsSuccess);
|
||||
default:
|
||||
return false;
|
||||
if(operation == ExpressionType.And || operation == ExpressionType.AndAlso)
|
||||
{
|
||||
isSuccess = true;
|
||||
}
|
||||
|
||||
foreach(var ruleFunc in ruleFuncList)
|
||||
{
|
||||
var ruleResult = ruleFunc(paramArray);
|
||||
resultList.Add(ruleResult);
|
||||
switch (operation)
|
||||
{
|
||||
case ExpressionType.And:
|
||||
case ExpressionType.AndAlso:
|
||||
isSuccess = isSuccess && ruleResult.IsSuccess;
|
||||
if(_reSettings.NestedRuleExecutionMode == NestedRuleExecutionMode.Performance && isSuccess == false)
|
||||
{
|
||||
return (isSuccess, resultList);
|
||||
}
|
||||
break;
|
||||
|
||||
case ExpressionType.Or:
|
||||
case ExpressionType.OrElse:
|
||||
isSuccess = isSuccess || ruleResult.IsSuccess;
|
||||
if (_reSettings.NestedRuleExecutionMode == NestedRuleExecutionMode.Performance && isSuccess == true)
|
||||
{
|
||||
return (isSuccess, resultList);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
return (isSuccess, resultList);
|
||||
}
|
||||
|
||||
internal Func<object[],Dictionary<string,object>> CompileScopedParams(RuleExpressionType ruleExpressionType, RuleParameter[] ruleParameters,RuleExpressionParameter[] ruleExpParams)
|
||||
{
|
||||
return GetExpressionBuilder(ruleExpressionType).CompileScopedParams(ruleParameters, ruleExpParams);
|
||||
|
||||
}
|
||||
|
||||
private RuleFunc<RuleResultTree> GetWrappedRuleFunc(Rule rule, RuleFunc<RuleResultTree> ruleFunc,RuleParameter[] ruleParameters,RuleExpressionParameter[] ruleExpParams)
|
||||
|
@ -221,7 +241,7 @@ namespace RulesEngine
|
|||
{
|
||||
return ruleFunc;
|
||||
}
|
||||
var paramDelegate = GetExpressionBuilder(rule.RuleExpressionType).CompileScopedParams(ruleParameters, ruleExpParams);
|
||||
var paramDelegate = CompileScopedParams(rule.RuleExpressionType,ruleParameters, ruleExpParams);
|
||||
|
||||
return (ruleParams) => {
|
||||
var inputs = ruleParams.Select(c => c.Value).ToArray();
|
||||
|
@ -232,21 +252,14 @@ namespace RulesEngine
|
|||
scopedParams = scopedParamsDict.Select(c => new RuleParameter(c.Key, c.Value));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
var resultFn = Helpers.ToResultTree(_reSettings, rule, null, (args) => false, $"Error while executing scoped params for rule `{rule.RuleName}` - {ex}");
|
||||
{
|
||||
var message = $"Error while executing scoped params for rule `{rule.RuleName}` - {ex}";
|
||||
var resultFn = Helpers.ToRuleExceptionResult(_reSettings, rule, new RuleException(message, ex));
|
||||
return resultFn(ruleParams);
|
||||
}
|
||||
|
||||
var extendedInputs = ruleParams.Concat(scopedParams);
|
||||
var result = ruleFunc(extendedInputs.ToArray());
|
||||
// To be removed in next major release
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if(result.RuleEvaluatedParams == null)
|
||||
{
|
||||
result.RuleEvaluatedParams = scopedParams;
|
||||
}
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using RulesEngine.HelperFunctions;
|
||||
using RulesEngine.Models;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
|
@ -13,40 +14,39 @@ namespace RulesEngine
|
|||
internal class RulesCache
|
||||
{
|
||||
/// <summary>The compile rules</summary>
|
||||
private ConcurrentDictionary<string, IDictionary<string, RuleFunc<RuleResultTree>>> _compileRules = new ConcurrentDictionary<string, IDictionary<string, RuleFunc<RuleResultTree>>>();
|
||||
private readonly MemCache _compileRules;
|
||||
|
||||
/// <summary>The workflow rules</summary>
|
||||
private ConcurrentDictionary<string, WorkflowRules> _workflowRules = new ConcurrentDictionary<string, WorkflowRules>();
|
||||
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>
|
||||
/// <param name="workflowName">Name of the workflow.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if [contains workflow rules] [the specified workflow name]; otherwise, <c>false</c>.</returns>
|
||||
public bool ContainsWorkflowRules(string workflowName)
|
||||
public bool ContainsWorkflows(string workflowName)
|
||||
{
|
||||
return _workflowRules.ContainsKey(workflowName);
|
||||
return _workflow.ContainsKey(workflowName);
|
||||
}
|
||||
|
||||
public List<string> GetAllWorkflowNames()
|
||||
{
|
||||
return _workflowRules.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);
|
||||
return _workflow.Keys.ToList();
|
||||
}
|
||||
|
||||
/// <summary>Adds the or update workflow rules.</summary>
|
||||
/// <param name="workflowName">Name of the workflow.</param>
|
||||
/// <param name="rules">The rules.</param>
|
||||
public void AddOrUpdateWorkflowRules(string workflowName, WorkflowRules rules)
|
||||
public void AddOrUpdateWorkflows(string workflowName, Workflow rules)
|
||||
{
|
||||
_workflowRules.AddOrUpdate(workflowName, rules, (k, v) => rules);
|
||||
long ticks = DateTime.UtcNow.Ticks;
|
||||
_workflow.AddOrUpdate(workflowName, (rules, ticks), (k, v) => (rules, ticks));
|
||||
}
|
||||
|
||||
/// <summary>Adds the or update compiled rule.</summary>
|
||||
|
@ -54,45 +54,67 @@ namespace RulesEngine
|
|||
/// <param name="compiledRule">The compiled rule.</param>
|
||||
public void AddOrUpdateCompiledRule(string compiledRuleKey, IDictionary<string, RuleFunc<RuleResultTree>> compiledRule)
|
||||
{
|
||||
_compileRules.AddOrUpdate(compiledRuleKey, compiledRule, (k, v) => compiledRule);
|
||||
long ticks = DateTime.UtcNow.Ticks;
|
||||
_compileRules.Set(compiledRuleKey,(compiledRule, ticks));
|
||||
}
|
||||
|
||||
/// <summary>Checks if the compiled rules are up-to-date.</summary>
|
||||
/// <param name="compiledRuleKey">The compiled rule key.</param>
|
||||
/// <param name="workflowName">The workflow name.</param>
|
||||
/// <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)
|
||||
{
|
||||
if (_compileRules.TryGetValue(compiledRuleKey, out (IDictionary<string, RuleFunc<RuleResultTree>> rules, long tick) compiledRulesObj))
|
||||
{
|
||||
if (_workflow.TryGetValue(workflowName, out (Workflow rules, long tick) WorkflowsObj))
|
||||
{
|
||||
return compiledRulesObj.tick >= WorkflowsObj.tick;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Clears this instance.</summary>
|
||||
public void Clear()
|
||||
{
|
||||
_workflowRules.Clear();
|
||||
_workflow.Clear();
|
||||
_compileRules.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Gets the work flow rules.</summary>
|
||||
/// <param name="workflowName">Name of the workflow.</param>
|
||||
/// <returns>WorkflowRules.</returns>
|
||||
/// <returns>Workflows.</returns>
|
||||
/// <exception cref="Exception">Could not find injected Workflow: {wfname}</exception>
|
||||
public WorkflowRules GetWorkFlowRules(string workflowName)
|
||||
public Workflow GetWorkflow(string workflowName)
|
||||
{
|
||||
_workflowRules.TryGetValue(workflowName, out var workflowRules);
|
||||
if (workflowRules == null) return null;
|
||||
else
|
||||
if (_workflow.TryGetValue(workflowName, out (Workflow rules, long tick) WorkflowsObj))
|
||||
{
|
||||
if (workflowRules.WorkflowRulesToInject?.Any() == true)
|
||||
var workflow = WorkflowsObj.rules;
|
||||
if (workflow.WorkflowsToInject?.Any() == true)
|
||||
{
|
||||
if (workflowRules.Rules == null)
|
||||
if (workflow.Rules == null)
|
||||
{
|
||||
workflowRules.Rules = new List<Rule>();
|
||||
workflow.Rules = new List<Rule>();
|
||||
}
|
||||
foreach (string wfname in workflowRules.WorkflowRulesToInject)
|
||||
foreach (string wfname in workflow.WorkflowsToInject)
|
||||
{
|
||||
var injectedWorkflow = GetWorkFlowRules(wfname);
|
||||
var injectedWorkflow = GetWorkflow(wfname);
|
||||
if (injectedWorkflow == null)
|
||||
{
|
||||
throw new Exception($"Could not find injected Workflow: {wfname}");
|
||||
}
|
||||
|
||||
workflowRules.Rules.ToList().AddRange(injectedWorkflow.Rules);
|
||||
workflow.Rules = workflow.Rules.Concat(injectedWorkflow.Rules).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
return workflowRules;
|
||||
return workflow;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,19 +124,19 @@ namespace RulesEngine
|
|||
/// <returns>CompiledRule.</returns>
|
||||
public IDictionary<string, RuleFunc<RuleResultTree>> GetCompiledRules(string compiledRulesKey)
|
||||
{
|
||||
return _compileRules[compiledRulesKey];
|
||||
return _compileRules.Get<(IDictionary<string, RuleFunc<RuleResultTree>> rules, long tick)>(compiledRulesKey).rules;
|
||||
}
|
||||
|
||||
/// <summary>Removes the specified workflow name.</summary>
|
||||
/// <param name="workflowName">Name of the workflow.</param>
|
||||
public void Remove(string workflowName)
|
||||
{
|
||||
if (_workflowRules.TryRemove(workflowName, out WorkflowRules workflowObj))
|
||||
if (_workflow.TryRemove(workflowName, out var workflowObj))
|
||||
{
|
||||
var compiledKeysToRemove = _compileRules.Keys.Where(key => key.StartsWith(workflowName));
|
||||
var compiledKeysToRemove = _compileRules.GetKeys().Where(key => key.StartsWith(workflowName));
|
||||
foreach (var key in compiledKeysToRemove)
|
||||
{
|
||||
_compileRules.TryRemove(key, out IDictionary<string, RuleFunc<RuleResultTree>> val);
|
||||
_compileRules.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,385 +1,444 @@
|
|||
// 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.ExpressionBuilders;
|
||||
using RulesEngine.Interfaces;
|
||||
using RulesEngine.Models;
|
||||
using RulesEngine.Validators;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RulesEngine
|
||||
{
|
||||
/// <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 RuleExpressionParser _ruleExpressionParser;
|
||||
private readonly RuleCompiler _ruleCompiler;
|
||||
private readonly ActionFactory _actionFactory;
|
||||
private const string ParamParseRegex = "(\\$\\(.*?\\))";
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
public RulesEngine(string[] jsonConfig, ILogger logger = null, ReSettings reSettings = null) : this(logger, reSettings)
|
||||
{
|
||||
var 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);
|
||||
_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);
|
||||
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)
|
||||
{
|
||||
var 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);
|
||||
}
|
||||
}
|
||||
|
||||
public List<string> GetAllRegisteredWorkflowNames()
|
||||
{
|
||||
return _rulesCache.GetAllWorkflowNames();
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
var 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 workflowRules.Rules.Where(c => c.Enabled))
|
||||
{
|
||||
dictFunc.Add(rule.RuleName, CompileRule(rule, ruleParams, workflowRules.GlobalParams?.ToArray()));
|
||||
}
|
||||
|
||||
_rulesCache.AddOrUpdateCompiledRule(compileRulesKey, dictFunc);
|
||||
_logger.LogTrace($"Rules has been compiled for the {workflowName} workflow and added to dictionary");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private RuleFunc<RuleResultTree> CompileRule(string workflowName, string ruleName, RuleParameter[] ruleParameters)
|
||||
{
|
||||
var workflow = _rulesCache.GetWorkFlowRules(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)
|
||||
{
|
||||
_logger.LogTrace($"Compiled rules found for {workflowName} workflow and executed");
|
||||
|
||||
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) }
|
||||
};
|
||||
}
|
||||
|
||||
/// <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
|
||||
}
|
||||
}
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using FluentValidation;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using RulesEngine.Actions;
|
||||
using RulesEngine.Exceptions;
|
||||
using RulesEngine.ExpressionBuilders;
|
||||
using RulesEngine.Extensions;
|
||||
using RulesEngine.HelperFunctions;
|
||||
using RulesEngine.Interfaces;
|
||||
using RulesEngine.Models;
|
||||
using RulesEngine.Validators;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RulesEngine
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <seealso cref="RulesEngine.Interfaces.IRulesEngine" />
|
||||
public class RulesEngine : IRulesEngine
|
||||
{
|
||||
#region Variables
|
||||
private readonly ReSettings _reSettings;
|
||||
private readonly RulesCache _rulesCache;
|
||||
private readonly RuleExpressionParser _ruleExpressionParser;
|
||||
private readonly RuleCompiler _ruleCompiler;
|
||||
private readonly ActionFactory _actionFactory;
|
||||
private const string ParamParseRegex = "(\\$\\(.*?\\))";
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
public RulesEngine(string[] jsonConfig, ReSettings reSettings = null) : this(reSettings)
|
||||
{
|
||||
var workflow = jsonConfig.Select(item => JsonConvert.DeserializeObject<Workflow>(item)).ToArray();
|
||||
AddWorkflow(workflow);
|
||||
}
|
||||
|
||||
public RulesEngine(Workflow[] Workflows, ReSettings reSettings = null) : this(reSettings)
|
||||
{
|
||||
AddWorkflow(Workflows);
|
||||
}
|
||||
|
||||
public RulesEngine(ReSettings reSettings = null)
|
||||
{
|
||||
_reSettings = reSettings == null ? new ReSettings(): new ReSettings(reSettings);
|
||||
if(_reSettings.CacheConfig == null)
|
||||
{
|
||||
_reSettings.CacheConfig = new MemCacheConfig();
|
||||
}
|
||||
_rulesCache = new RulesCache(_reSettings);
|
||||
_ruleExpressionParser = new RuleExpressionParser(_reSettings);
|
||||
_ruleCompiler = new RuleCompiler(new RuleExpressionBuilderFactory(_reSettings, _ruleExpressionParser),_reSettings);
|
||||
_actionFactory = new ActionFactory(GetActionRegistry(_reSettings));
|
||||
}
|
||||
|
||||
private IDictionary<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)
|
||||
{
|
||||
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 sortedRuleParams = ruleParams.ToList();
|
||||
sortedRuleParams.Sort((RuleParameter a, RuleParameter b) => string.Compare(a.Name, b.Name));
|
||||
var ruleResultList = ValidateWorkflowAndExecuteRule(workflowName, sortedRuleParams.ToArray());
|
||||
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>
|
||||
/// Checks is workflow exist.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
return _rulesCache.ContainsWorkflows(workflowName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the workflow.
|
||||
/// </summary>
|
||||
public void ClearWorkflows()
|
||||
{
|
||||
_rulesCache.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the workflows.
|
||||
/// </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
|
||||
{
|
||||
// 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>>();
|
||||
if (_reSettings.AutoRegisterInputType)
|
||||
{
|
||||
_reSettings.CustomTypes = _reSettings.CustomTypes.Safe().Union(ruleParams.Select(c => c.Type)).ToArray();
|
||||
}
|
||||
// add separate compilation for global params
|
||||
|
||||
var globalParamExp = new Lazy<RuleExpressionParameter[]>(
|
||||
() => _ruleCompiler.GetRuleExpressionParameters(workflow.RuleExpressionType, workflow.GlobalParams, ruleParams)
|
||||
);
|
||||
|
||||
foreach (var rule in workflow.Rules.Where(c => c.Enabled))
|
||||
{
|
||||
dictFunc.Add(rule.RuleName, CompileRule(rule,workflow.RuleExpressionType, ruleParams, globalParamExp));
|
||||
}
|
||||
|
||||
_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}`");
|
||||
}
|
||||
var globalParamExp = new Lazy<RuleExpressionParameter[]>(
|
||||
() => _ruleCompiler.GetRuleExpressionParameters(workflow.RuleExpressionType, workflow.GlobalParams, ruleParameters)
|
||||
);
|
||||
return CompileRule(currentRule,workflow.RuleExpressionType, ruleParameters, globalParamExp);
|
||||
}
|
||||
|
||||
private RuleFunc<RuleResultTree> CompileRule(Rule rule, RuleExpressionType ruleExpressionType, RuleParameter[] ruleParams, Lazy<RuleExpressionParameter[]> scopedParams)
|
||||
{
|
||||
return _ruleCompiler.CompileRule(rule, ruleExpressionType, 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 ruleParamsKey = string.Join("-", ruleParams.Select(c => $"{c.Name}_{c.Type.Name}"));
|
||||
var key = $"{workflowName}-" + ruleParamsKey;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<Version>3.1.0-preview.4</Version>
|
||||
<TargetFrameworks>net8.0;net6.0;netstandard2.0</TargetFrameworks>
|
||||
<Version>5.0.3</Version>
|
||||
<Copyright>Copyright (c) Microsoft Corporation.</Copyright>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<PackageProjectUrl>https://github.com/microsoft/RulesEngine</PackageProjectUrl>
|
||||
<Authors>Abbas Cyclewala, Dishant Munjal, Yogesh Prajapati</Authors>
|
||||
<Authors>Abbas Cyclewala</Authors>
|
||||
<Description>Rules Engine is a package for abstracting business logic/rules/policies out of the system. This works in a very simple way by giving you an ability to put your rules in a store outside the core logic of the system thus ensuring that any change in rules doesn't affect the core system.</Description>
|
||||
<PackageReleaseNotes>https://github.com/microsoft/RulesEngine/blob/main/CHANGELOG.md</PackageReleaseNotes>
|
||||
<PackageTags>BRE, Rules Engine, Abstraction</PackageTags>
|
||||
<PackageReleaseNotes>https://github.com/microsoft/RulesEngine/blob/main/CHANGELOG.md</PackageReleaseNotes>
|
||||
<PackageTags>BRE, Rules Engine, Abstraction</PackageTags>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
|
@ -19,22 +20,28 @@
|
|||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<SignAssembly>True</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>..\..\signing\RulesEngine-publicKey.snk</AssemblyOriginatorKeyFile>
|
||||
<DelaySign>True</DelaySign>
|
||||
<Deterministic>true</Deterministic>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\LICENSE" Pack="true" PackagePath="" />
|
||||
<None Include="..\..\LICENSE" Pack="true" PackagePath="\" />
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FastExpressionCompiler" Version="2.0.0" />
|
||||
<PackageReference Include="FluentValidation" Version="9.4.0" />
|
||||
<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.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
|
||||
<PackageReference Include="FastExpressionCompiler" Version="4.0.1" />
|
||||
<PackageReference Include="FluentValidation" Version="11.9.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.3.7" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||
<PackageReference Include="Microsoft.CSharp" Version="4.7.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -26,7 +26,7 @@ namespace RulesEngine.Validators
|
|||
.WithMessage(Constants.OPERATOR_INCORRECT_ERRMSG);
|
||||
|
||||
When(c => c.Rules?.Any() != true, () => {
|
||||
RuleFor(c => c.WorkflowRulesToInject).NotEmpty().WithMessage(Constants.INJECT_WORKFLOW_RULES_ERRMSG);
|
||||
RuleFor(c => c.WorkflowsToInject).NotEmpty().WithMessage(Constants.INJECT_WORKFLOW_RULES_ERRMSG);
|
||||
})
|
||||
.Otherwise(() => {
|
||||
RuleFor(c => c.Rules).Must(BeValidRulesList);
|
||||
|
@ -39,11 +39,11 @@ namespace RulesEngine.Validators
|
|||
{
|
||||
When(c => c.Operator == null && c.RuleExpressionType == RuleExpressionType.LambdaExpression, () => {
|
||||
RuleFor(c => c.Expression).NotEmpty().WithMessage(Constants.LAMBDA_EXPRESSION_EXPRESSION_NULL_ERRMSG);
|
||||
RuleFor(c => c.Rules).Null().WithMessage(Constants.LAMBDA_EXPRESSION_RULES_ERRMSG);
|
||||
RuleFor(c => c.Rules).Empty().WithMessage(Constants.OPERATOR_RULES_ERRMSG);
|
||||
});
|
||||
}
|
||||
|
||||
private bool BeValidRulesList(List<Rule> rules)
|
||||
private bool BeValidRulesList(IEnumerable<Rule> rules)
|
||||
{
|
||||
if (rules?.Any() != true) return false;
|
||||
var validator = new RuleValidator();
|
||||
|
|
|
@ -8,13 +8,13 @@ using System.Linq;
|
|||
|
||||
namespace RulesEngine.Validators
|
||||
{
|
||||
internal class WorkflowRulesValidator : AbstractValidator<WorkflowRules>
|
||||
internal class WorkflowsValidator : AbstractValidator<Workflow>
|
||||
{
|
||||
public WorkflowRulesValidator()
|
||||
public WorkflowsValidator()
|
||||
{
|
||||
RuleFor(c => c.WorkflowName).NotEmpty().WithMessage(Constants.WORKFLOW_NAME_NULL_ERRMSG);
|
||||
When(c => c.Rules?.Any() != true, () => {
|
||||
RuleFor(c => c.WorkflowRulesToInject).NotEmpty().WithMessage(Constants.INJECT_WORKFLOW_RULES_ERRMSG);
|
||||
RuleFor(c => c.WorkflowsToInject).NotEmpty().WithMessage(Constants.INJECT_WORKFLOW_RULES_ERRMSG);
|
||||
}).Otherwise(() => {
|
||||
var ruleValidator = new RuleValidator();
|
||||
RuleForEach(c => c.Rules).SetValidator(ruleValidator);
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using RulesEngine.Models;
|
||||
using RulesEngine.UnitTest.ActionTests.MockClass;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace RulesEngine.UnitTest.ActionTests
|
||||
{
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class CustomActionTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task CustomActionOnRuleMustHaveContextValues()
|
||||
{
|
||||
var workflow = GetWorkflow();
|
||||
var re = new RulesEngine(workflow, reSettings: new ReSettings {
|
||||
CustomActions = new Dictionary<string, System.Func<Actions.ActionBase>> {
|
||||
|
||||
{ "ReturnContext", () => new ReturnContextAction() }
|
||||
}
|
||||
});
|
||||
|
||||
var result = await re.ExecuteAllRulesAsync("successReturnContextAction", true);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task CustomAction_WithSystemTextJsobOnRuleMustHaveContextValues()
|
||||
{
|
||||
var workflow = GetWorkflow();
|
||||
var workflowStr = JsonConvert.SerializeObject(workflow);
|
||||
var serializationOptions = new System.Text.Json.JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } };
|
||||
var workflowViaTextJson = System.Text.Json.JsonSerializer.Deserialize<Workflow[]>(workflowStr,serializationOptions);
|
||||
|
||||
|
||||
var re = new RulesEngine(workflow, reSettings: new ReSettings {
|
||||
CustomActions = new Dictionary<string, System.Func<Actions.ActionBase>> {
|
||||
|
||||
{ "ReturnContext", () => new ReturnContextAction() }
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
var result = await re.ExecuteAllRulesAsync("successReturnContextAction", true);
|
||||
}
|
||||
|
||||
private Workflow[] GetWorkflow()
|
||||
{
|
||||
return new Workflow[] {
|
||||
new Workflow {
|
||||
WorkflowName = "successReturnContextAction",
|
||||
Rules = new Rule[] {
|
||||
new Rule {
|
||||
RuleName = "trueRule",
|
||||
Expression = "input1 == true",
|
||||
Actions = new RuleActions() {
|
||||
OnSuccess = new ActionInfo {
|
||||
Name = "ReturnContext",
|
||||
Context = new Dictionary<string, object> {
|
||||
{"stringContext", "hello"},
|
||||
{"intContext",1 },
|
||||
{"objectContext", new { a = "hello", b = 123 } }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using RulesEngine.Actions;
|
||||
using RulesEngine.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RulesEngine.UnitTest.ActionTests.MockClass
|
||||
{
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class ReturnContextAction : ActionBase
|
||||
{
|
||||
public override ValueTask<object> Run(ActionContext context, RuleParameter[] ruleParameters)
|
||||
{
|
||||
var stringContext = context.GetContext<string>("stringContext");
|
||||
var intContext = context.GetContext<int>("intContext");
|
||||
var objectContext = context.GetContext<object>("objectContext");
|
||||
|
||||
return new ValueTask<object>(new {
|
||||
stringContext,
|
||||
intContext,
|
||||
objectContext
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using RulesEngine.Enums;
|
||||
using RulesEngine.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
@ -25,6 +23,16 @@ namespace RulesEngine.UnitTest
|
|||
Assert.Equal(2 * 2, result.Output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WhenExpressionIsSuccess_ComplexOutputExpressionAction_ReturnsExpressionEvaluation()
|
||||
{
|
||||
var engine = new RulesEngine(GetWorkflowWithActions());
|
||||
var result = await engine.ExecuteActionWorkflowAsync("ActionWorkflow", "ComplexOutputRuleTest", new RuleParameter[0]);
|
||||
Assert.NotNull(result);
|
||||
dynamic output = result.Output;
|
||||
Assert.Equal(2, output.test);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WhenExpressionIsSuccess_EvaluateRuleAction_ReturnsExpressionEvaluation()
|
||||
{
|
||||
|
@ -48,16 +56,36 @@ namespace RulesEngine.UnitTest
|
|||
public async Task ExecuteActionWorkflowAsync_CalledWithNoActionsInWorkflow_ExecutesSuccessfully()
|
||||
{
|
||||
|
||||
var engine = new RulesEngine(GetWorkflowRulesWithoutActions());
|
||||
var engine = new RulesEngine(GetWorkflowsWithoutActions());
|
||||
var result = await engine.ExecuteActionWorkflowAsync("NoActionWorkflow", "NoActionTest", new RuleParameter[0]);
|
||||
Assert.NotNull(result);
|
||||
Assert.Null(result.Output);
|
||||
}
|
||||
|
||||
|
||||
private WorkflowRules[] GetWorkflowRulesWithoutActions()
|
||||
[Fact]
|
||||
public async Task ExecuteActionWorkflowAsync_SelfReferencingAction_NoFilter_ExecutesSuccessfully()
|
||||
{
|
||||
var workflow1 = new WorkflowRules {
|
||||
|
||||
var engine = new RulesEngine(GetWorkflowWithActions());
|
||||
var result = await engine.ExecuteActionWorkflowAsync("WorkflowWithGlobalsAndSelfRefActions", "RuleReferencingSameWorkflow", new RuleParameter[0]);
|
||||
Assert.NotNull(result);
|
||||
Assert.Null(result.Output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteActionWorkflowAsync_SelfReferencingAction_WithFilter_ExecutesSuccessfully()
|
||||
{
|
||||
|
||||
var engine = new RulesEngine(GetWorkflowWithActions());
|
||||
var result = await engine.ExecuteActionWorkflowAsync("WorkflowWithGlobalsAndSelfRefActions", "RuleReferencingSameWorkflowWithInputFilter", new RuleParameter[0]);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(4,result.Output);
|
||||
}
|
||||
|
||||
private Workflow[] GetWorkflowsWithoutActions()
|
||||
{
|
||||
var workflow1 = new Workflow {
|
||||
WorkflowName = "NoActionWorkflow",
|
||||
Rules = new List<Rule>{
|
||||
new Rule{
|
||||
|
@ -71,43 +99,122 @@ namespace RulesEngine.UnitTest
|
|||
return new[] { workflow1 };
|
||||
}
|
||||
|
||||
private WorkflowRules[] GetWorkflowWithActions()
|
||||
private Workflow[] GetWorkflowWithActions()
|
||||
{
|
||||
|
||||
var workflow1 = new WorkflowRules {
|
||||
var workflow1 = new Workflow {
|
||||
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{
|
||||
Actions = new RuleActions{
|
||||
OnSuccess = new ActionInfo{
|
||||
Name = "OutputExpression",
|
||||
Context = new Dictionary<string, object>{
|
||||
{"expression", "2*2"}
|
||||
}
|
||||
}}
|
||||
}
|
||||
}
|
||||
},
|
||||
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{
|
||||
RuleName = "EvaluateRuleTest",
|
||||
RuleExpressionType = RuleExpressionType.LambdaExpression,
|
||||
Expression = "1 == 1",
|
||||
Actions = new Dictionary<ActionTriggerType, ActionInfo>{
|
||||
{ ActionTriggerType.onSuccess, new ActionInfo{
|
||||
Actions = new RuleActions{
|
||||
OnSuccess = new ActionInfo{
|
||||
Name = "EvaluateRule",
|
||||
Context = new Dictionary<string, object>{
|
||||
{"workflowName", "ActionWorkflow"},
|
||||
{"ruleName","ExpressionOutputRuleTest"}
|
||||
}
|
||||
}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
return new[] { workflow1 };
|
||||
|
||||
var workflow2 = new Workflow {
|
||||
WorkflowName = "WorkflowWithGlobalsAndSelfRefActions",
|
||||
GlobalParams = new[] {
|
||||
new ScopedParam {
|
||||
Name = "global1",
|
||||
Expression = "\"Hello\""
|
||||
}
|
||||
},
|
||||
Rules = new[] {
|
||||
|
||||
new Rule{
|
||||
RuleName = "RuleReferencingSameWorkflow",
|
||||
Expression = "1 == 1",
|
||||
Actions = new RuleActions {
|
||||
OnSuccess = new ActionInfo{
|
||||
Name = "EvaluateRule",
|
||||
Context = new Dictionary<string, object>{
|
||||
{"workflowName", "WorkflowWithGlobalsAndSelfRefActions"},
|
||||
{"ruleName","OtherRule"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},new Rule{
|
||||
RuleName = "RuleReferencingSameWorkflowWithInputFilter",
|
||||
Expression = "1 == 1",
|
||||
Actions = new RuleActions {
|
||||
OnSuccess = new ActionInfo{
|
||||
Name = "EvaluateRule",
|
||||
Context = new Dictionary<string, object>{
|
||||
{"workflowName", "WorkflowWithGlobalsAndSelfRefActions"},
|
||||
{"ruleName","OtherRule"},
|
||||
{"inputFilter",new string[] { } },
|
||||
{"additionalInputs", new [] {
|
||||
new ScopedParam(){
|
||||
Name = "additionalValue",
|
||||
Expression = "1"
|
||||
}
|
||||
|
||||
} }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
, new Rule{
|
||||
RuleName = "OtherRule",
|
||||
Expression = "additionalValue == 1",
|
||||
Actions = new RuleActions {
|
||||
OnSuccess = new ActionInfo{
|
||||
Name = "OutputExpression",
|
||||
Context = new Dictionary<string, object>{
|
||||
{"expression", "2*2"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
return new[] { workflow1, workflow2 };
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,53 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using RulesEngine.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace RulesEngine.UnitTest
|
||||
{
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class CaseSensitiveTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(true,true,false)]
|
||||
[InlineData(false,true,true)]
|
||||
public async Task CaseSensitiveTest(bool caseSensitive, bool expected1, bool expected2)
|
||||
{
|
||||
var reSettings = new ReSettings {
|
||||
IsExpressionCaseSensitive = caseSensitive
|
||||
};
|
||||
|
||||
|
||||
var worflow = new Workflow {
|
||||
WorkflowName = "CaseSensitivityTest",
|
||||
Rules = new[] {
|
||||
new Rule {
|
||||
RuleName = "check same case1",
|
||||
Expression = "input1 == \"hello\""
|
||||
},
|
||||
new Rule {
|
||||
RuleName = "check same case2",
|
||||
Expression = "INPUT1 == \"hello\""
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var re = new RulesEngine(new[] { worflow }, reSettings);
|
||||
var result = await re.ExecuteAllRulesAsync("CaseSensitivityTest", "hello");
|
||||
|
||||
Assert.Equal(expected1, result[0].IsSuccess);
|
||||
Assert.Equal(expected2, result[1].IsSuccess);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using RulesEngine.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace RulesEngine.UnitTest
|
||||
{
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class EmptyRulesTest
|
||||
{
|
||||
[Fact]
|
||||
private async Task EmptyRules_ReturnsExepectedResults()
|
||||
{
|
||||
var workflow = GetEmptyWorkflow();
|
||||
var reSettings = new ReSettings { };
|
||||
RulesEngine rulesEngine = new RulesEngine();
|
||||
|
||||
Func<Task> action = () => {
|
||||
new RulesEngine(workflow, reSettings: reSettings);
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
Exception ex = await Assert.ThrowsAsync<Exceptions.RuleValidationException>(action);
|
||||
|
||||
Assert.Contains("Atleast one of Rules or WorkflowsToInject must be not empty", ex.Message);
|
||||
}
|
||||
[Fact]
|
||||
private async Task NestedRulesWithEmptyNestedActions_ReturnsExepectedResults()
|
||||
{
|
||||
var workflow = GetEmptyNestedWorkflows();
|
||||
var reSettings = new ReSettings { };
|
||||
RulesEngine rulesEngine = new RulesEngine();
|
||||
|
||||
Func<Task> action = () => {
|
||||
new RulesEngine(workflow, reSettings: reSettings);
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
Exception ex = await Assert.ThrowsAsync<Exceptions.RuleValidationException>(action);
|
||||
|
||||
Assert.Contains("Atleast one of Rules or WorkflowsToInject must be not empty", ex.Message);
|
||||
}
|
||||
|
||||
private Workflow[] GetEmptyWorkflow()
|
||||
{
|
||||
return new[] {
|
||||
new Workflow {
|
||||
WorkflowName = "EmptyRulesTest",
|
||||
Rules = new Rule[] {
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Workflow[] GetEmptyNestedWorkflows()
|
||||
{
|
||||
return new[] {
|
||||
new Workflow {
|
||||
WorkflowName = "EmptyNestedRulesTest",
|
||||
Rules = new Rule[] {
|
||||
new Rule {
|
||||
RuleName = "AndRuleTrueFalse",
|
||||
Operator = "And",
|
||||
Rules = new Rule[] {
|
||||
new Rule{
|
||||
RuleName = "trueRule1",
|
||||
Expression = "input1.TrueValue == true",
|
||||
},
|
||||
new Rule {
|
||||
RuleName = "falseRule1",
|
||||
Expression = "input1.TrueValue == false"
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
new Rule {
|
||||
RuleName = "OrRuleTrueFalse",
|
||||
Operator = "Or",
|
||||
Rules = new Rule[] {
|
||||
new Rule{
|
||||
RuleName = "trueRule2",
|
||||
Expression = "input1.TrueValue == true",
|
||||
},
|
||||
new Rule {
|
||||
RuleName = "falseRule2",
|
||||
Expression = "input1.TrueValue == false"
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
new Rule {
|
||||
RuleName = "AndRuleFalseTrue",
|
||||
Operator = "And",
|
||||
Rules = new Rule[] {
|
||||
new Rule{
|
||||
RuleName = "trueRule3",
|
||||
Expression = "input1.TrueValue == false",
|
||||
},
|
||||
new Rule {
|
||||
RuleName = "falseRule4",
|
||||
Expression = "input1.TrueValue == true"
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
new Rule {
|
||||
RuleName = "OrRuleFalseTrue",
|
||||
Operator = "Or",
|
||||
Rules = new Rule[] {
|
||||
new Rule{
|
||||
RuleName = "trueRule3",
|
||||
Expression = "input1.TrueValue == false",
|
||||
},
|
||||
new Rule {
|
||||
RuleName = "falseRule4",
|
||||
Expression = "input1.TrueValue == true"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
new Workflow {
|
||||
WorkflowName = "EmptyNestedRulesActionsTest",
|
||||
Rules = new Rule[] {
|
||||
new Rule {
|
||||
RuleName = "AndRuleTrueFalse",
|
||||
Operator = "And",
|
||||
Rules = new Rule[] {
|
||||
|
||||
},
|
||||
Actions = new RuleActions {
|
||||
OnFailure = new ActionInfo{
|
||||
Name = "OutputExpression",
|
||||
Context = new Dictionary<string, object> {
|
||||
{ "Expression", "input1.TrueValue" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ using RulesEngine.ExpressionBuilders;
|
|||
using RulesEngine.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace RulesEngine.UnitTest
|
||||
|
@ -39,7 +40,7 @@ namespace RulesEngine.UnitTest
|
|||
Expression = "RequestType == \"vod\""
|
||||
};
|
||||
|
||||
mainRule.Rules.Add(dummyRule);
|
||||
mainRule.Rules = mainRule.Rules.Append(dummyRule);
|
||||
var func = builder.BuildDelegateForRule(dummyRule, ruleParameters);
|
||||
|
||||
Assert.NotNull(func);
|
||||
|
|
|
@ -49,7 +49,7 @@ namespace RulesEngine.UnitTest
|
|||
successEventName = eventName;
|
||||
});
|
||||
|
||||
Assert.True(successEventName.Equals("Test Rule 1"));
|
||||
Assert.Equal("Test Rule 1", successEventName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -89,7 +89,7 @@ namespace RulesEngine.UnitTest
|
|||
successEventName = eventName;
|
||||
});
|
||||
|
||||
Assert.True(successEventName.Equals("Event 1"));
|
||||
Assert.Equal("Event 1", successEventName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -128,7 +128,7 @@ namespace RulesEngine.UnitTest
|
|||
successEventName = eventName;
|
||||
});
|
||||
|
||||
Assert.True(successEventName.Equals(string.Empty));
|
||||
Assert.Equal(successEventName, string.Empty);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,226 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using RulesEngine.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace RulesEngine.UnitTest
|
||||
{
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class NestedRulesTest
|
||||
{
|
||||
|
||||
[Theory]
|
||||
[InlineData(NestedRuleExecutionMode.All)]
|
||||
[InlineData(NestedRuleExecutionMode.Performance)]
|
||||
public async Task NestedRulesShouldFollowExecutionMode(NestedRuleExecutionMode mode)
|
||||
{
|
||||
var workflow = GetWorkflow();
|
||||
var reSettings = new ReSettings { NestedRuleExecutionMode = mode };
|
||||
var rulesEngine = new RulesEngine(workflow, reSettings: reSettings);
|
||||
dynamic input1 = new ExpandoObject();
|
||||
input1.trueValue = true;
|
||||
|
||||
List<RuleResultTree> result = await rulesEngine.ExecuteAllRulesAsync("NestedRulesTest", input1);
|
||||
var andResults = result.Where(c => c.Rule.Operator == "And").ToList();
|
||||
var orResults = result.Where(c => c.Rule.Operator == "Or").ToList();
|
||||
Assert.All(andResults,
|
||||
c => Assert.False(c.IsSuccess)
|
||||
);
|
||||
Assert.All(orResults,
|
||||
c => Assert.True(c.IsSuccess));
|
||||
|
||||
if (mode == NestedRuleExecutionMode.All)
|
||||
{
|
||||
Assert.All(andResults,
|
||||
c => Assert.Equal(c.Rule.Rules.Count(), c.ChildResults.Count()));
|
||||
Assert.All(orResults,
|
||||
c => Assert.Equal(c.Rule.Rules.Count(), c.ChildResults.Count()));
|
||||
}
|
||||
else if (mode == NestedRuleExecutionMode.Performance)
|
||||
{
|
||||
Assert.All(andResults,
|
||||
c => {
|
||||
Assert.Equal(c.IsSuccess, c.ChildResults.Last().IsSuccess);
|
||||
Assert.Single(c.ChildResults.Where(d => c.IsSuccess == d.IsSuccess));
|
||||
Assert.True(c.ChildResults.SkipLast(1).All(d => d.IsSuccess == true));
|
||||
});
|
||||
|
||||
Assert.All(orResults,
|
||||
c => {
|
||||
Assert.Equal(c.IsSuccess, c.ChildResults.Last().IsSuccess);
|
||||
Assert.Single(c.ChildResults.Where(d => c.IsSuccess == d.IsSuccess));
|
||||
Assert.True(c.ChildResults.SkipLast(1).All(d => d.IsSuccess == false));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
private async Task NestedRulesWithNestedActions_ReturnsCorrectResults()
|
||||
{
|
||||
var workflow = GetWorkflow();
|
||||
var reSettings = new ReSettings { };
|
||||
var rulesEngine = new RulesEngine(workflow, reSettings: reSettings);
|
||||
dynamic input1 = new ExpandoObject();
|
||||
input1.trueValue = true;
|
||||
|
||||
List<RuleResultTree> result = await rulesEngine.ExecuteAllRulesAsync("NestedRulesActionsTest", input1);
|
||||
|
||||
Assert.False(result[0].IsSuccess);
|
||||
Assert.Equal(input1.trueValue, result[0].ActionResult.Output);
|
||||
Assert.All(result[0].ChildResults, (childResult) => Assert.Equal(input1.trueValue, childResult.ActionResult.Output));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
private async Task NestedRulesWithNestedActions_WorkflowParsedWithSystemTextJson_ReturnsCorrectResults()
|
||||
{
|
||||
var workflow = GetWorkflow();
|
||||
var workflowStr = JsonConvert.SerializeObject(workflow);
|
||||
|
||||
var serializationOptions = new System.Text.Json.JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } };
|
||||
|
||||
|
||||
var workflowViaTextJson = System.Text.Json.JsonSerializer.Deserialize<Workflow[]>(workflowStr, serializationOptions);
|
||||
|
||||
var reSettings = new ReSettings { };
|
||||
var rulesEngine = new RulesEngine(workflowViaTextJson, reSettings: reSettings);
|
||||
dynamic input1 = new ExpandoObject();
|
||||
input1.trueValue = true;
|
||||
|
||||
List<RuleResultTree> result = await rulesEngine.ExecuteAllRulesAsync("NestedRulesActionsTest", input1);
|
||||
|
||||
Assert.False(result[0].IsSuccess);
|
||||
Assert.Equal(input1.trueValue, result[0].ActionResult.Output);
|
||||
Assert.All(result[0].ChildResults, (childResult) => Assert.Equal(input1.trueValue, childResult.ActionResult.Output));
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private Workflow[] GetWorkflow()
|
||||
{
|
||||
return new[] {
|
||||
new Workflow {
|
||||
WorkflowName = "NestedRulesTest",
|
||||
Rules = new Rule[] {
|
||||
new Rule {
|
||||
RuleName = "AndRuleTrueFalse",
|
||||
Operator = "And",
|
||||
Rules = new Rule[] {
|
||||
new Rule{
|
||||
RuleName = "trueRule1",
|
||||
Expression = "input1.TrueValue == true",
|
||||
},
|
||||
new Rule {
|
||||
RuleName = "falseRule1",
|
||||
Expression = "input1.TrueValue == false"
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
new Rule {
|
||||
RuleName = "OrRuleTrueFalse",
|
||||
Operator = "Or",
|
||||
Rules = new Rule[] {
|
||||
new Rule{
|
||||
RuleName = "trueRule2",
|
||||
Expression = "input1.TrueValue == true",
|
||||
},
|
||||
new Rule {
|
||||
RuleName = "falseRule2",
|
||||
Expression = "input1.TrueValue == false"
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
new Rule {
|
||||
RuleName = "AndRuleFalseTrue",
|
||||
Operator = "And",
|
||||
Rules = new Rule[] {
|
||||
new Rule{
|
||||
RuleName = "trueRule3",
|
||||
Expression = "input1.TrueValue == false",
|
||||
},
|
||||
new Rule {
|
||||
RuleName = "falseRule4",
|
||||
Expression = "input1.TrueValue == true"
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
new Rule {
|
||||
RuleName = "OrRuleFalseTrue",
|
||||
Operator = "Or",
|
||||
Rules = new Rule[] {
|
||||
new Rule{
|
||||
RuleName = "trueRule3",
|
||||
Expression = "input1.TrueValue == false",
|
||||
},
|
||||
new Rule {
|
||||
RuleName = "falseRule4",
|
||||
Expression = "input1.TrueValue == true"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
new Workflow {
|
||||
WorkflowName = "NestedRulesActionsTest",
|
||||
Rules = new Rule[] {
|
||||
new Rule {
|
||||
RuleName = "AndRuleTrueFalse",
|
||||
Operator = "And",
|
||||
Rules = new Rule[] {
|
||||
new Rule{
|
||||
RuleName = "trueRule1",
|
||||
Expression = "input1.TrueValue == true",
|
||||
Actions = new RuleActions {
|
||||
OnSuccess = new ActionInfo{
|
||||
Name = "OutputExpression",
|
||||
Context = new Dictionary<string, object> {
|
||||
{ "Expression", "input1.TrueValue" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
new Rule {
|
||||
RuleName = "falseRule1",
|
||||
Expression = "input1.TrueValue == false",
|
||||
Actions = new RuleActions {
|
||||
OnFailure = new ActionInfo{
|
||||
Name = "OutputExpression",
|
||||
Context = new Dictionary<string, object> {
|
||||
{ "Expression", "input1.TrueValue" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Actions = new RuleActions {
|
||||
OnFailure = new ActionInfo{
|
||||
Name = "OutputExpression",
|
||||
Context = new Dictionary<string, object> {
|
||||
{ "Expression", "input1.TrueValue" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using RulesEngine.Models;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace RulesEngine.UnitTest
|
||||
{
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class ParameterNameChangeTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task RunTwiceTest_ReturnsExpectedResults()
|
||||
{
|
||||
var workflow = new Workflow {
|
||||
WorkflowName = "ParameterNameChangeWorkflow",
|
||||
Rules = new Rule[] {
|
||||
new Rule {
|
||||
RuleName = "ParameterNameChangeRule",
|
||||
RuleExpressionType = RuleExpressionType.LambdaExpression,
|
||||
Expression = "test.blah == 1"
|
||||
}
|
||||
}
|
||||
};
|
||||
var engine = new RulesEngine();
|
||||
engine.AddOrUpdateWorkflow(workflow);
|
||||
|
||||
dynamic dynamicBlah = new ExpandoObject();
|
||||
dynamicBlah.blah = (Int64)1;
|
||||
var input_pass = new RuleParameter("test", dynamicBlah);
|
||||
var input_fail = new RuleParameter("SOME_OTHER_NAME", dynamicBlah);
|
||||
// RuleParameter name matches expression, so should pass.
|
||||
var pass_results = await engine.ExecuteAllRulesAsync("ParameterNameChangeWorkflow", input_pass);
|
||||
// RuleParameter name DOES NOT MATCH expression, so should fail.
|
||||
var fail_results = await engine.ExecuteAllRulesAsync("ParameterNameChangeWorkflow", input_fail);
|
||||
Assert.True(pass_results.First().IsSuccess);
|
||||
Assert.False(fail_results.First().IsSuccess);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using RulesEngine.ExpressionBuilders;
|
||||
using RulesEngine.Models;
|
||||
using System;
|
||||
|
@ -17,10 +16,10 @@ namespace RulesEngine.UnitTest
|
|||
[Fact]
|
||||
public void RuleCompiler_NullCheck()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new RuleCompiler(null, null,null));
|
||||
Assert.Throws<ArgumentNullException>(() => new RuleCompiler(null, null));
|
||||
var reSettings = new 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]
|
||||
|
@ -28,11 +27,9 @@ namespace RulesEngine.UnitTest
|
|||
{
|
||||
var reSettings = new ReSettings();
|
||||
var parser = new RuleExpressionParser(reSettings);
|
||||
var compiler = new RuleCompiler(new RuleExpressionBuilderFactory(reSettings, parser),null, new NullLogger<RuleCompiler>());
|
||||
Assert.Throws<ArgumentNullException>(() => compiler.CompileRule(null, null,null));
|
||||
Assert.Throws<ArgumentNullException>(() => compiler.CompileRule(null, new RuleParameter[] { null },null));
|
||||
var compiler = new RuleCompiler(new RuleExpressionBuilderFactory(reSettings, parser),null);
|
||||
Assert.Throws<ArgumentNullException>(() => compiler.CompileRule(null, RuleExpressionType.LambdaExpression,null,null));
|
||||
Assert.Throws<ArgumentNullException>(() => compiler.CompileRule(null, RuleExpressionType.LambdaExpression, new RuleParameter[] { null },null));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
using RulesEngine.ExpressionBuilders;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Xunit;
|
||||
|
||||
namespace RulesEngine.UnitTest.RuleExpressionParserTests
|
||||
{
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class RuleExpressionParserTests
|
||||
{
|
||||
public RuleExpressionParserTests() {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void TestExpressionWithJObject()
|
||||
{
|
||||
var ruleParser = new RuleExpressionParser(new Models.ReSettings());
|
||||
|
||||
var inputStr = @"{
|
||||
""list"": [
|
||||
{ ""item1"": ""hello"",
|
||||
""item3"": 1
|
||||
},
|
||||
{
|
||||
""item2"": ""world""
|
||||
}
|
||||
]
|
||||
}";
|
||||
|
||||
|
||||
var input = JObject.Parse(inputStr);
|
||||
|
||||
|
||||
var value = ruleParser.Evaluate<object>("input.list[0].item3 == 1", new[] { new Models.RuleParameter("input", input) });
|
||||
|
||||
Assert.Equal(true,
|
||||
value);
|
||||
|
||||
|
||||
var value2 = ruleParser.Evaluate<object>("input.list[1].item2 == \"world\"", new[] { new Models.RuleParameter("input", input) });
|
||||
|
||||
Assert.Equal(true,
|
||||
value2);
|
||||
|
||||
|
||||
var value3= ruleParser.Evaluate<object>("string.Concat(input.list[0].item1,input.list[1].item2)", new[] { new Models.RuleParameter("input", input) });
|
||||
|
||||
Assert.Equal("helloworld", value3);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
public void TestExpressionWithDifferentCompilerSettings(bool fastExpressionEnabled){
|
||||
var ruleParser = new RuleExpressionParser(new Models.ReSettings() { UseFastExpressionCompiler = fastExpressionEnabled });
|
||||
|
||||
decimal? d1 = null;
|
||||
var result = ruleParser.Evaluate<bool>("d1 < 20", new[] { Models.RuleParameter.Create("d1", d1) });
|
||||
Assert.False(result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -12,8 +12,8 @@ namespace RulesEngine.UnitTest
|
|||
[JsonProperty("country")]
|
||||
public string Country { get; set; }
|
||||
|
||||
[JsonProperty("loyalityFactor")]
|
||||
public int LoyalityFactor { get; set; }
|
||||
[JsonProperty("loyaltyFactor")]
|
||||
public int loyaltyFactor { get; set; }
|
||||
public int TotalPurchasesToDate { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using RulesEngine.HelperFunctions;
|
||||
using RulesEngine.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace RulesEngine.UnitTest
|
||||
{
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class RuleValidationTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task NullExpressionithLambdaExpression_ReturnsExepectedResults()
|
||||
{
|
||||
var workflow = GetNullExpressionithLambdaExpressionWorkflow();
|
||||
var reSettings = new ReSettings { };
|
||||
RulesEngine rulesEngine = new RulesEngine();
|
||||
|
||||
Func<Task> action = () => {
|
||||
new RulesEngine(workflow, reSettings: reSettings);
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
Exception ex = await Assert.ThrowsAsync<Exceptions.RuleValidationException>(action);
|
||||
|
||||
Assert.Contains(Constants.LAMBDA_EXPRESSION_EXPRESSION_NULL_ERRMSG, ex.Message);
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NestedRulesWithMissingOperator_ReturnsExepectedResults()
|
||||
{
|
||||
var workflow = GetEmptyOperatorWorkflow();
|
||||
var reSettings = new ReSettings { };
|
||||
RulesEngine rulesEngine = new RulesEngine();
|
||||
|
||||
Func<Task> action = () => {
|
||||
new RulesEngine(workflow, reSettings: reSettings);
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
Exception ex = await Assert.ThrowsAsync<Exceptions.RuleValidationException>(action);
|
||||
|
||||
Assert.Contains(Constants.OPERATOR_RULES_ERRMSG, ex.Message);
|
||||
|
||||
}
|
||||
|
||||
private Workflow[] GetNullExpressionithLambdaExpressionWorkflow()
|
||||
{
|
||||
return new[] {
|
||||
new Workflow {
|
||||
WorkflowName = "NestedRulesTest",
|
||||
Rules = new Rule[] {
|
||||
new Rule {
|
||||
RuleName = "TestRule",
|
||||
RuleExpressionType = RuleExpressionType.LambdaExpression,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Workflow[] GetEmptyOperatorWorkflow()
|
||||
{
|
||||
return new[] {
|
||||
new Workflow {
|
||||
WorkflowName = "NestedRulesTest",
|
||||
Rules = new Rule[] {
|
||||
new Rule {
|
||||
RuleName = "AndRuleTrueFalse",
|
||||
Expression = "true == true",
|
||||
Rules = new Rule[] {
|
||||
new Rule{
|
||||
RuleName = "trueRule1",
|
||||
Expression = "input1.TrueValue == true",
|
||||
},
|
||||
new Rule {
|
||||
RuleName = "falseRule1",
|
||||
Expression = "input1.TrueValue == false"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -22,8 +22,8 @@ namespace RulesEngine.UnitTest
|
|||
[InlineData("RuleEnabledNestedFeatureTest", new bool[] { true, true, false })]
|
||||
public async Task RulesEngine_ShouldOnlyExecuteEnabledRules(string workflowName, bool[] expectedRuleResults)
|
||||
{
|
||||
var workflows = GetWorkflows();
|
||||
var rulesEngine = new RulesEngine(workflows);
|
||||
var workflow = GetWorkflows();
|
||||
var rulesEngine = new RulesEngine(workflow, reSettings: new ReSettings() { EnableExceptionAsErrorMessage = false });
|
||||
var input1 = new {
|
||||
TrueValue = true
|
||||
};
|
||||
|
@ -45,7 +45,7 @@ namespace RulesEngine.UnitTest
|
|||
public async Task WorkflowUpdatedRuleEnabled_ShouldReflect(string workflowName, bool[] expectedRuleResults)
|
||||
{
|
||||
var workflow = GetWorkflows().Single(c => c.WorkflowName == workflowName);
|
||||
var rulesEngine = new RulesEngine();
|
||||
var rulesEngine = new RulesEngine(reSettings: new ReSettings() { EnableExceptionAsErrorMessage = false});
|
||||
rulesEngine.AddWorkflow(workflow);
|
||||
var input1 = new {
|
||||
TrueValue = true
|
||||
|
@ -95,10 +95,10 @@ namespace RulesEngine.UnitTest
|
|||
return areAllRulesEnabled;
|
||||
}
|
||||
|
||||
private WorkflowRules[] GetWorkflows()
|
||||
private Workflow[] GetWorkflows()
|
||||
{
|
||||
return new[] {
|
||||
new WorkflowRules {
|
||||
new Workflow {
|
||||
WorkflowName = "RuleEnabledFeatureTest",
|
||||
Rules = new List<Rule> {
|
||||
new Rule {
|
||||
|
@ -118,7 +118,7 @@ namespace RulesEngine.UnitTest
|
|||
|
||||
}
|
||||
},
|
||||
new WorkflowRules {
|
||||
new Workflow {
|
||||
WorkflowName = "RuleEnabledNestedFeatureTest",
|
||||
Rules = new List<Rule> {
|
||||
new Rule {
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<SignAssembly>True</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>..\..\signing\RulesEngine-publicKey.snk</AssemblyOriginatorKeyFile>
|
||||
<DelaySign>True</DelaySign>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoFixture" Version="4.15.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
|
||||
<PackageReference Include="Moq" Version="4.16.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<PackageReference Include="AutoFixture" Version="4.18.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.1" />
|
||||
<PackageReference Include="xunit" Version="2.6.5" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="3.0.2">
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
@ -23,6 +27,9 @@
|
|||
<None Update="TestData\rules1.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="TestData\rules11.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="TestData\rules4.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
|
@ -44,6 +51,9 @@
|
|||
<None Update="TestData\rules8.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="TestData\rules10.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="TestData\rules9.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the MIT License.
|
||||
|
||||
using RulesEngine.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
@ -10,6 +11,14 @@ using Xunit;
|
|||
|
||||
namespace RulesEngine.UnitTest
|
||||
{
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class MyObject
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public int Count { get; set; }
|
||||
}
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class ScopedParamsTest
|
||||
{
|
||||
|
@ -22,12 +31,12 @@ namespace RulesEngine.UnitTest
|
|||
[InlineData("GlobalParamReferencedInNextGlobalParams")]
|
||||
[InlineData("LocalParamReferencedInNextLocalParams")]
|
||||
[InlineData("GlobalParamAndLocalParamsInNestedRules")]
|
||||
public async Task BasicWorkflowRules_ReturnsTrue(string workflowName)
|
||||
public async Task BasicWorkflows_ReturnsTrue(string workflowName)
|
||||
{
|
||||
var workflows = GetWorkflowRulesList();
|
||||
var workflow = GetWorkflowList();
|
||||
|
||||
var engine = new RulesEngine(null, null);
|
||||
engine.AddWorkflow(workflows);
|
||||
var engine = new RulesEngine();
|
||||
engine.AddWorkflow(workflow);
|
||||
|
||||
var input1 = new {
|
||||
trueValue = true,
|
||||
|
@ -45,10 +54,10 @@ namespace RulesEngine.UnitTest
|
|||
[InlineData("GlobalAndLocalParams")]
|
||||
public async Task WorkflowUpdate_GlobalParam_ShouldReflect(string workflowName)
|
||||
{
|
||||
var workflows = GetWorkflowRulesList();
|
||||
var workflow = GetWorkflowList();
|
||||
|
||||
var engine = new RulesEngine(null, null);
|
||||
engine.AddWorkflow(workflows);
|
||||
var engine = new RulesEngine();
|
||||
engine.AddWorkflow(workflow);
|
||||
|
||||
var input1 = new {
|
||||
trueValue = true,
|
||||
|
@ -58,7 +67,7 @@ namespace RulesEngine.UnitTest
|
|||
var result = await engine.ExecuteAllRulesAsync(workflowName, input1);
|
||||
Assert.True(result.All(c => c.IsSuccess));
|
||||
|
||||
var workflowToUpdate = workflows.Single(c => c.WorkflowName == workflowName);
|
||||
var workflowToUpdate = workflow.Single(c => c.WorkflowName == workflowName);
|
||||
engine.RemoveWorkflow(workflowName);
|
||||
workflowToUpdate.GlobalParams.First().Expression = "true == false";
|
||||
engine.AddWorkflow(workflowToUpdate);
|
||||
|
@ -70,17 +79,17 @@ namespace RulesEngine.UnitTest
|
|||
|
||||
|
||||
[Theory]
|
||||
[InlineData("GlobalParamsOnly",new []{ false })]
|
||||
[InlineData("GlobalParamsOnly", new[] { false })]
|
||||
[InlineData("LocalParamsOnly", new[] { false, true })]
|
||||
[InlineData("GlobalAndLocalParams", new[] { false })]
|
||||
public async Task DisabledScopedParam_ShouldReflect(string workflowName, bool[] outputs)
|
||||
{
|
||||
var workflows = GetWorkflowRulesList();
|
||||
var workflow = GetWorkflowList();
|
||||
|
||||
var engine = new RulesEngine(new string[] { }, null, new ReSettings {
|
||||
var engine = new RulesEngine(new string[] { }, new ReSettings {
|
||||
EnableScopedParams = false
|
||||
});
|
||||
engine.AddWorkflow(workflows);
|
||||
engine.AddWorkflow(workflow);
|
||||
|
||||
var input1 = new {
|
||||
trueValue = true,
|
||||
|
@ -88,10 +97,10 @@ namespace RulesEngine.UnitTest
|
|||
};
|
||||
|
||||
var result = await engine.ExecuteAllRulesAsync(workflowName, input1);
|
||||
for(var i = 0; i < result.Count; i++)
|
||||
for (var i = 0; i < result.Count; i++)
|
||||
{
|
||||
Assert.Equal(result[i].IsSuccess, outputs[i]);
|
||||
if(result[i].IsSuccess == false)
|
||||
if (result[i].IsSuccess == false)
|
||||
{
|
||||
Assert.StartsWith("Exception while parsing expression", result[i].ExceptionMessage);
|
||||
}
|
||||
|
@ -100,24 +109,79 @@ namespace RulesEngine.UnitTest
|
|||
|
||||
[Theory]
|
||||
[InlineData("GlobalParamsOnly")]
|
||||
[InlineData("LocalParamsOnly")]
|
||||
[InlineData("LocalParamsOnly2")]
|
||||
[InlineData("GlobalParamsOnlyWithComplexInput")]
|
||||
public async Task ErrorInScopedParam_ShouldAppearAsErrorMessage(string workflowName)
|
||||
{
|
||||
var workflows = GetWorkflowRulesList();
|
||||
var workflow = GetWorkflowList();
|
||||
|
||||
var engine = new RulesEngine(new string[] { }, null);
|
||||
engine.AddWorkflow(workflows);
|
||||
engine.AddWorkflow(workflow);
|
||||
|
||||
var input = new { };
|
||||
var result = await engine.ExecuteAllRulesAsync(workflowName, input);
|
||||
|
||||
Assert.All(result, c => Assert.False(c.IsSuccess));
|
||||
Assert.All(result, c => {
|
||||
Assert.False(c.IsSuccess);
|
||||
Assert.StartsWith("Error while compiling rule", c.ExceptionMessage);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("GlobalParamsOnlyWithComplexInput")]
|
||||
[InlineData("LocalParamsOnlyWithComplexInput")]
|
||||
public async Task RuntimeErrorInScopedParam_ShouldAppearAsErrorMessage(string workflowName)
|
||||
{
|
||||
var workflow = GetWorkflowList();
|
||||
|
||||
var engine = new RulesEngine(new string[] { }, null);
|
||||
engine.AddWorkflow(workflow);
|
||||
|
||||
|
||||
|
||||
var input = new RuleTestClass();
|
||||
var result = await engine.ExecuteAllRulesAsync(workflowName, input);
|
||||
|
||||
Assert.All(result, c => {
|
||||
Assert.False(c.IsSuccess);
|
||||
Assert.StartsWith("Error while executing scoped params for rule", c.ExceptionMessage);
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("LocalParam_CorrectAnswer")]
|
||||
public async Task LocalParam_GivesCorrectAnswer(string workflowName)
|
||||
{
|
||||
var workflow = GetWorkflowList();
|
||||
|
||||
var reSettingsWithCustomTypes = new ReSettings { CustomTypes = new Type[] { } };
|
||||
var bre = new RulesEngine(workflow, reSettingsWithCustomTypes);
|
||||
|
||||
var myObject = new MyObject() {
|
||||
Name = "My Object",
|
||||
Count = 2
|
||||
};
|
||||
|
||||
var rp1 = new RuleParameter("myObj", myObject);
|
||||
|
||||
List<RuleResultTree> resultList = await bre.ExecuteAllRulesAsync(workflowName, rp1);
|
||||
Assert.True(resultList[0].IsSuccess);
|
||||
|
||||
myObject.Count = 3;
|
||||
|
||||
resultList = await bre.ExecuteAllRulesAsync(workflowName, rp1);
|
||||
Assert.False(resultList[0].IsSuccess);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void CheckResultTreeContainsAllInputs(string workflowName, List<RuleResultTree> result)
|
||||
{
|
||||
var workflow = GetWorkflowRulesList().Single(c => c.WorkflowName == workflowName);
|
||||
var workflow = GetWorkflowList().Single(c => c.WorkflowName == workflowName);
|
||||
var expectedInputs = new List<string>() { "input1" };
|
||||
expectedInputs.AddRange(workflow.GlobalParams?.Select(c => c.Name) ?? new List<string>());
|
||||
|
||||
|
@ -136,10 +200,6 @@ namespace RulesEngine.UnitTest
|
|||
var localParamNames = resultTree.Rule.LocalParams?.Select(c => c.Name) ?? new List<string>();
|
||||
Assert.All(localParamNames, input => Assert.True(resultTree.Inputs.ContainsKey(input)));
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Assert.All(localParamNames, lp => Assert.Contains(resultTree.RuleEvaluatedParams, c => c.Name == lp));
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
if (resultTree.ChildResults?.Any() == true)
|
||||
{
|
||||
foreach (var childResultTree in resultTree.ChildResults)
|
||||
|
@ -150,10 +210,10 @@ namespace RulesEngine.UnitTest
|
|||
}
|
||||
|
||||
}
|
||||
private WorkflowRules[] GetWorkflowRulesList()
|
||||
private Workflow[] GetWorkflowList()
|
||||
{
|
||||
return new WorkflowRules[] {
|
||||
new WorkflowRules {
|
||||
return new Workflow[] {
|
||||
new Workflow {
|
||||
WorkflowName = "NoLocalAndGlobalParams",
|
||||
Rules = new List<Rule> {
|
||||
new Rule {
|
||||
|
@ -162,7 +222,7 @@ namespace RulesEngine.UnitTest
|
|||
}
|
||||
}
|
||||
},
|
||||
new WorkflowRules {
|
||||
new Workflow {
|
||||
WorkflowName = "LocalParamsOnly",
|
||||
Rules = new List<Rule> {
|
||||
new Rule {
|
||||
|
@ -183,7 +243,24 @@ namespace RulesEngine.UnitTest
|
|||
},
|
||||
}
|
||||
},
|
||||
new WorkflowRules {
|
||||
new Workflow {
|
||||
WorkflowName = "LocalParamsOnly2",
|
||||
Rules = new List<Rule> {
|
||||
new Rule {
|
||||
|
||||
RuleName = "WithLocalParam",
|
||||
LocalParams = new List<ScopedParam> {
|
||||
new ScopedParam {
|
||||
Name = "localParam1",
|
||||
Expression = "input1.trueValue"
|
||||
}
|
||||
},
|
||||
Expression = "localParam1 == true"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
new Workflow {
|
||||
WorkflowName = "GlobalParamsOnly",
|
||||
GlobalParams = new List<ScopedParam> {
|
||||
new ScopedParam {
|
||||
|
@ -198,7 +275,7 @@ namespace RulesEngine.UnitTest
|
|||
}
|
||||
}
|
||||
},
|
||||
new WorkflowRules {
|
||||
new Workflow {
|
||||
WorkflowName = "GlobalAndLocalParams",
|
||||
GlobalParams = new List<ScopedParam> {
|
||||
new ScopedParam {
|
||||
|
@ -220,7 +297,7 @@ namespace RulesEngine.UnitTest
|
|||
}
|
||||
|
||||
},
|
||||
new WorkflowRules {
|
||||
new Workflow {
|
||||
WorkflowName = "GlobalParamReferencedInLocalParams",
|
||||
GlobalParams = new List<ScopedParam> {
|
||||
new ScopedParam {
|
||||
|
@ -242,7 +319,7 @@ namespace RulesEngine.UnitTest
|
|||
},
|
||||
}
|
||||
},
|
||||
new WorkflowRules {
|
||||
new Workflow {
|
||||
WorkflowName = "GlobalParamReferencedInNextGlobalParams",
|
||||
GlobalParams = new List<ScopedParam> {
|
||||
new ScopedParam {
|
||||
|
@ -261,7 +338,7 @@ namespace RulesEngine.UnitTest
|
|||
},
|
||||
}
|
||||
},
|
||||
new WorkflowRules {
|
||||
new Workflow {
|
||||
WorkflowName = "LocalParamReferencedInNextLocalParams",
|
||||
Rules = new List<Rule> {
|
||||
new Rule {
|
||||
|
@ -280,7 +357,7 @@ namespace RulesEngine.UnitTest
|
|||
},
|
||||
}
|
||||
},
|
||||
new WorkflowRules {
|
||||
new Workflow {
|
||||
WorkflowName = "GlobalParamAndLocalParamsInNestedRules",
|
||||
GlobalParams = new List<ScopedParam> {
|
||||
new ScopedParam {
|
||||
|
@ -296,7 +373,7 @@ namespace RulesEngine.UnitTest
|
|||
new ScopedParam {
|
||||
Name = "localParam1",
|
||||
Expression = @"""world"""
|
||||
}
|
||||
}
|
||||
},
|
||||
Rules = new List<Rule>{
|
||||
new Rule{
|
||||
|
@ -318,6 +395,67 @@ namespace RulesEngine.UnitTest
|
|||
|
||||
}
|
||||
}
|
||||
},
|
||||
new Workflow {
|
||||
WorkflowName = "LocalParamsOnlyWithComplexInput",
|
||||
Rules = new List<Rule> {
|
||||
new Rule {
|
||||
|
||||
RuleName = "WithLocalParam",
|
||||
LocalParams = new List<ScopedParam> {
|
||||
new ScopedParam {
|
||||
Name = "localParam1",
|
||||
Expression = "input1.Country.ToLower()"
|
||||
}
|
||||
},
|
||||
Expression = "localParam1 == \"hello\""
|
||||
}
|
||||
}
|
||||
},
|
||||
new Workflow {
|
||||
WorkflowName = "GlobalParamsOnlyWithComplexInput",
|
||||
GlobalParams = new List<ScopedParam> {
|
||||
new ScopedParam {
|
||||
Name = "globalParam1",
|
||||
Expression = "input1.Country.ToLower()"
|
||||
}
|
||||
},
|
||||
Rules = new List<Rule> {
|
||||
new Rule {
|
||||
RuleName = "TrueTest",
|
||||
Expression = "globalParam1 == \"hello\""
|
||||
},
|
||||
new Rule {
|
||||
RuleName = "TrueTest2",
|
||||
Expression = "globalParam1.ToUpper() == \"HELLO\""
|
||||
}
|
||||
}
|
||||
},
|
||||
new Workflow {
|
||||
WorkflowName = "LocalParam_CorrectAnswer",
|
||||
Rules = new List<Rule> {
|
||||
new Rule
|
||||
{
|
||||
RuleName = "Test Rule",
|
||||
LocalParams = new List<LocalParam>
|
||||
{
|
||||
new LocalParam
|
||||
{
|
||||
Name = "threshold",
|
||||
Expression = "3"
|
||||
},
|
||||
new LocalParam
|
||||
{
|
||||
Name = "myList",
|
||||
Expression = "new int[]{ 1, 2, 3, 4, 5 }"
|
||||
}
|
||||
},
|
||||
SuccessEvent = "Count is within tolerance.",
|
||||
ErrorMessage = "Not as expected.",
|
||||
Expression = "myList.Where(x => x < threshold).Contains(myObj.Count)",
|
||||
RuleExpressionType = RuleExpressionType.LambdaExpression
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.country == \"canada\" AND input1.loyalityFactor <= 4"
|
||||
"Expression": "input1.country == \"canada\" AND input1.loyaltyFactor <= 4"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"WorkflowName": "inputWorkflow",
|
||||
"Rules": [
|
||||
{
|
||||
"RuleName": "GiveDiscount10",
|
||||
"SuccessEvent": "10",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.Data.GetProperty(\"category\").GetString() == \"abc\""
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"WorkflowName": "MyWorkflow",
|
||||
"WorkflowsToInject": null,
|
||||
"RuleExpressionType": 0,
|
||||
"GlobalParams": [
|
||||
{
|
||||
"Name": "threshold",
|
||||
"Expression": "double.Parse(\u00220.25\u0022)"
|
||||
}
|
||||
],
|
||||
"Rules": [
|
||||
{
|
||||
"RuleName": "Activation",
|
||||
"Properties": null,
|
||||
"Operator": null,
|
||||
"ErrorMessage": null,
|
||||
"Enabled": true,
|
||||
"RuleExpressionType": 0,
|
||||
"WorkflowsToInject": null,
|
||||
"Rules": null,
|
||||
"LocalParams": [
|
||||
{
|
||||
"Name": "ruleCount",
|
||||
"Expression": "int.Parse(\u002215\u0022)"
|
||||
}
|
||||
],
|
||||
"Expression": "input1.Count \u003E= ruleCount \u0026\u0026 input1.Where(x =\u003E x.Value \u003E= threshold).Count() \u003E= ruleCount",
|
||||
"Actions": null,
|
||||
"SuccessEvent": null
|
||||
},
|
||||
{
|
||||
"RuleName": "Deactivation",
|
||||
"Properties": null,
|
||||
"Operator": null,
|
||||
"ErrorMessage": null,
|
||||
"Enabled": true,
|
||||
"RuleExpressionType": 0,
|
||||
"WorkflowsToInject": null,
|
||||
"Rules": null,
|
||||
"LocalParams": [
|
||||
{
|
||||
"Name": "ruleCount",
|
||||
"Expression": "int.Parse(\u002230\u0022)"
|
||||
}
|
||||
],
|
||||
"Expression": "input1.Count \u003E= ruleCount \u0026\u0026 input1.OrderByDescending(o =\u003E o.ChangeDateTime).Take(ruleCount).All(a =\u003E a.Value \u003C threshold)",
|
||||
"Actions": null,
|
||||
"SuccessEvent": null
|
||||
}
|
||||
]
|
||||
}
|
|
@ -13,7 +13,7 @@
|
|||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.country == \"india\" AND input1.loyalityFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2"
|
||||
"Expression": "input1.country == \"india\" AND input1.loyaltyFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2"
|
||||
},
|
||||
{
|
||||
"RuleName": "GiveDiscount20",
|
||||
|
@ -21,7 +21,7 @@
|
|||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.country == \"india\" AND input1.loyalityFactor == 3 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2"
|
||||
"Expression": "input1.country == \"india\" AND input1.loyaltyFactor == 3 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2"
|
||||
},
|
||||
{
|
||||
"RuleName": "GiveDiscount25",
|
||||
|
@ -29,7 +29,7 @@
|
|||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.country != \"india\" AND input1.loyalityFactor >= 2 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 5"
|
||||
"Expression": "input1.country != \"india\" AND input1.loyaltyFactor >= 2 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 5"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input1.couy == \"india\" AND input1.loyalityFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input2.noOfVisitsPerMonth > 2"
|
||||
"Expression": "input1.couy == \"india\" AND input1.loyaltyFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input2.noOfVisitsPerMonth > 2"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
{
|
||||
"RuleName": "GiveDiscount10",
|
||||
"SuccessEvent": "10",
|
||||
"ErrorMessage": "One or more adjust rules failed, with loyalityFactor : $(model1.loyalityFactor), country : $(model1.country), totalPurchasesToDate : $(model1.totalPurchasesToDate), model2 : $(model2)",
|
||||
"ErrorMessage": "One or more adjust rules failed, with loyaltyFactor : $(model1.loyaltyFactor), country : $(model1.country), totalPurchasesToDate : $(model1.totalPurchasesToDate), model2 : $(model2)",
|
||||
"ErrorType": "Error",
|
||||
"localParams": [
|
||||
{
|
||||
|
@ -18,7 +18,7 @@
|
|||
}
|
||||
],
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "model1.country == \"india\" AND model1.loyalityFactor <= 2 AND model1.totalPurchasesToDate >= 5000 AND model2"
|
||||
"Expression": "model1.country == \"india\" AND model1.loyaltyFactor <= 2 AND model1.totalPurchasesToDate >= 5000 AND model2"
|
||||
},
|
||||
{
|
||||
"RuleName": "GiveDiscount100",
|
||||
|
@ -35,12 +35,12 @@
|
|||
}
|
||||
],
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "model1.country == \"india\" AND model1.loyalityFactor < 0 AND model1.totalPurchasesToDate >= 5000 AND model2"
|
||||
"Expression": "model1.country == \"india\" AND model1.loyaltyFactor < 0 AND model1.totalPurchasesToDate >= 5000 AND model2"
|
||||
},
|
||||
{
|
||||
"RuleName": "GiveDiscount25",
|
||||
"SuccessEvent": "25",
|
||||
"ErrorMessage": "One or more adjust rules failed, country : $(input4.country), loyalityFactor : $(input4.loyalityFactor), totalPurchasesToDate : $(input4.totalPurchasesToDate), totalOrders : $(input5.totalOrders), noOfVisitsPerMonth : $(input30.noOfVisitsPerMonth)",
|
||||
"ErrorMessage": "One or more adjust rules failed, country : $(input4.country), loyaltyFactor : $(input4.loyaltyFactor), totalPurchasesToDate : $(input4.totalPurchasesToDate), totalOrders : $(input5.totalOrders), noOfVisitsPerMonth : $(input30.noOfVisitsPerMonth), $(model2)",
|
||||
"ErrorType": "Error",
|
||||
"localParams": [
|
||||
{
|
||||
|
@ -53,7 +53,7 @@
|
|||
}
|
||||
],
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input4.country == \"india\" AND input4.loyalityFactor >= 2 AND input4.totalPurchasesToDate <= 10 AND input5.totalOrders > 2 AND input3.noOfVisitsPerMonth > 5"
|
||||
"Expression": "input4.country == \"india\" AND input4.loyaltyFactor >= 2 AND input4.totalPurchasesToDate <= 10 AND input5.totalOrders > 2 AND input3.noOfVisitsPerMonth > 5"
|
||||
},
|
||||
{
|
||||
"RuleName": "GiveDiscount30",
|
||||
|
@ -61,7 +61,7 @@
|
|||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input4.loyalityFactor > 30 AND input4.totalPurchasesToDate >= 50000 AND input4.totalPurchasesToDate <= 100000 AND input5.totalOrders > 5 AND input3.noOfVisitsPerMonth > 15"
|
||||
"Expression": "input4.loyaltyFactor > 30 AND input4.totalPurchasesToDate >= 50000 AND input4.totalPurchasesToDate <= 100000 AND input5.totalOrders > 5 AND input3.noOfVisitsPerMonth > 15"
|
||||
},
|
||||
{
|
||||
"RuleName": "GiveDiscount35",
|
||||
|
@ -69,7 +69,7 @@
|
|||
"ErrorMessage": "One or more adjust rules failed, totalPurchasesToDate : $(input4.totalPurchasesToDate), totalOrders : $(input5.totalOrders)",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "input4.loyalityFactor > 30 AND input4.totalPurchasesToDate >= 100000 AND input5.totalOrders > 15 AND input3.noOfVisitsPerMonth > 25"
|
||||
"Expression": "input4.loyaltyFactor > 30 AND input4.totalPurchasesToDate >= 100000 AND input5.totalOrders > 15 AND input3.noOfVisitsPerMonth > 25"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -2,12 +2,20 @@
|
|||
"WorkflowName": "inputWorkflow",
|
||||
"Rules": [
|
||||
{
|
||||
"RuleName": "GiveDiscount10",
|
||||
"RuleName": "upperCaseAccess",
|
||||
"SuccessEvent": "10",
|
||||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "utils.CheckExists(String(input1.Property1)) == true"
|
||||
},
|
||||
{
|
||||
"RuleName": "lowerCaseAccess",
|
||||
"SuccessEvent": "10",
|
||||
"ErrorMessage": "One or more adjust rules failed.",
|
||||
"ErrorType": "Error",
|
||||
"RuleExpressionType": "LambdaExpression",
|
||||
"Expression": "utils.CheckExists(String(input1.property1)) == true"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using RulesEngine.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace RulesEngine.UnitTest
|
||||
{
|
||||
[Trait("Category", "Unit")]
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class TypedClassTests
|
||||
{
|
||||
public class Transazione
|
||||
{
|
||||
public static string StaticProperty { get; set; } = "Hello";
|
||||
public List<Attore> Attori { get; set; } = new();
|
||||
}
|
||||
public class Attore
|
||||
{
|
||||
public Guid Id { get; internal set; }
|
||||
public string Nome { get; internal set; }
|
||||
public RuoloAttore RuoloAttore { get; internal set; }
|
||||
}
|
||||
|
||||
public enum RuoloAttore
|
||||
{
|
||||
A,
|
||||
B,
|
||||
C
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TypedClassTest()
|
||||
{
|
||||
Workflow workflow = new() {
|
||||
WorkflowName = "Conferimento",
|
||||
Rules = new Rule[] {
|
||||
new() {
|
||||
RuleName = "Attore Da",
|
||||
Enabled = true,
|
||||
ErrorMessage = "Attore Da Id must be defined",
|
||||
SuccessEvent = "10",
|
||||
RuleExpressionType = RuleExpressionType.LambdaExpression,
|
||||
Expression = "transazione.Attori.Any(a => a.RuoloAttore == 1)",
|
||||
},
|
||||
new() {
|
||||
RuleName = "Attore A",
|
||||
Enabled = true,
|
||||
ErrorMessage = "Attore A must be defined",
|
||||
SuccessEvent = "10",
|
||||
RuleExpressionType = RuleExpressionType.LambdaExpression,
|
||||
Expression = "transazione.Attori != null",
|
||||
},
|
||||
}
|
||||
};
|
||||
var reSettings = new ReSettings() {
|
||||
CustomTypes = new Type[] {
|
||||
},
|
||||
AutoRegisterInputType = false
|
||||
};
|
||||
var re = new RulesEngine(reSettings);
|
||||
re.AddWorkflow(workflow);
|
||||
|
||||
var param = new Transazione {
|
||||
Attori = new List<Attore>{
|
||||
new Attore{
|
||||
RuoloAttore = RuoloAttore.B,
|
||||
|
||||
},
|
||||
new Attore {
|
||||
RuoloAttore = RuoloAttore.C
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
var result = await re.ExecuteAllRulesAsync("Conferimento", new RuleParameter("transazione", param));
|
||||
|
||||
Assert.All(result, (res) => Assert.True(res.IsSuccess));
|
||||
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task TypedClassInputSameNameAsTypeTest()
|
||||
{
|
||||
Workflow workflow = new() {
|
||||
WorkflowName = "Conferimento",
|
||||
Rules = new Rule[] {
|
||||
new() {
|
||||
RuleName = "Attore Da",
|
||||
Enabled = true,
|
||||
ErrorMessage = "Attore Da Id must be defined",
|
||||
SuccessEvent = "10",
|
||||
RuleExpressionType = RuleExpressionType.LambdaExpression,
|
||||
Expression = "transazione.Attori.Any(a => a.RuoloAttore == 1)",
|
||||
},
|
||||
new() {
|
||||
RuleName = "Attore A",
|
||||
Enabled = true,
|
||||
ErrorMessage = "Attore A must be defined",
|
||||
SuccessEvent = "10",
|
||||
RuleExpressionType = RuleExpressionType.LambdaExpression,
|
||||
Expression = "transazione.Attori != null",
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
var reSettings = new ReSettings() {
|
||||
CustomTypes = new Type[] {
|
||||
typeof(Transazione)
|
||||
}
|
||||
};
|
||||
var re = new RulesEngine(reSettings);
|
||||
re.AddWorkflow(workflow);
|
||||
|
||||
var param = new Transazione {
|
||||
Attori = new List<Attore>{
|
||||
new Attore{
|
||||
RuoloAttore = RuoloAttore.B,
|
||||
|
||||
},
|
||||
new Attore {
|
||||
RuoloAttore = RuoloAttore.C
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
var result = await re.ExecuteAllRulesAsync("Conferimento", new RuleParameter("Transazione", param));
|
||||
|
||||
Assert.All(result, (res) => Assert.True(res.IsSuccess));
|
||||
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task TypedClassBothAccessibleTestWhenCaseInsensitive()
|
||||
{
|
||||
Workflow workflow = new() {
|
||||
WorkflowName = "Conferimento",
|
||||
Rules = new Rule[] {
|
||||
new() {
|
||||
RuleName = "Attore Da",
|
||||
Enabled = true,
|
||||
ErrorMessage = "Attore Da Id must be defined",
|
||||
SuccessEvent = "10",
|
||||
RuleExpressionType = RuleExpressionType.LambdaExpression,
|
||||
Expression = "transazione.Attori.Any(a => a.RuoloAttore == 1)",
|
||||
},
|
||||
new() {
|
||||
RuleName = "Attore A",
|
||||
Enabled = true,
|
||||
ErrorMessage = "Attore A must be defined",
|
||||
SuccessEvent = "10",
|
||||
RuleExpressionType = RuleExpressionType.LambdaExpression,
|
||||
Expression = "transazione.Attori != null",
|
||||
},
|
||||
new() {
|
||||
RuleName = "Static FieldTest",
|
||||
Expression = "Transazione.StaticProperty == \"Hello\""
|
||||
}
|
||||
}
|
||||
};
|
||||
var reSettings = new ReSettings() {
|
||||
CustomTypes = new Type[] {
|
||||
typeof(Transazione)
|
||||
},
|
||||
IsExpressionCaseSensitive = true
|
||||
};
|
||||
var re = new RulesEngine(reSettings);
|
||||
re.AddWorkflow(workflow);
|
||||
|
||||
var param = new Transazione {
|
||||
Attori = new List<Attore>{
|
||||
new Attore{
|
||||
RuoloAttore = RuoloAttore.B,
|
||||
|
||||
},
|
||||
new Attore {
|
||||
RuoloAttore = RuoloAttore.C
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
var result = await re.ExecuteAllRulesAsync("Conferimento", new RuleParameter("transazione", param));
|
||||
|
||||
Assert.All(result, (res) => Assert.True(res.IsSuccess));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
using RulesEngine.HelperFunctions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
@ -61,6 +62,20 @@ namespace RulesEngine.UnitTest
|
|||
Assert.NotNull(typedobj.GetType().GetProperty("Test"));
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void GetJObject_nonDynamicObject()
|
||||
{
|
||||
dynamic obj = JObject.FromObject(new {
|
||||
Test = "hello"
|
||||
});
|
||||
dynamic typedobj = Utils.GetTypedObject(obj);
|
||||
Assert.IsNotType<ExpandoObject>(typedobj);
|
||||
Assert.IsType<JObject>(typedobj);
|
||||
Assert.NotNull(typedobj.Test);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void CreateObject_dynamicObject()
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue