Compare commits

...

64 Commits
v3.3.0 ... main

Author SHA1 Message Date
dependabot[bot] dc52989895
Bump github/codeql-action from 2 to 3 (#572)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-17 02:27:49 +05:30
dependabot[bot] be6faef9f0
Bump actions/setup-dotnet from 3 to 4 (#560)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 3 to 4.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Abbas Cyclewala <abcy@microsoft.com>
2024-01-13 15:24:34 +05:30
Abbas Cyclewala 9b53a9aa3a
Users/abbasc52/dotnet8 (#571)
* updated to dotnet 8

* fixed readme formatting

* removed unnecessary dependencies
2024-01-13 15:20:51 +05:30
Abbas Cyclewala 22353a38d0
Abbasc52/optional fast compile (#570)
* added setting to turn off fast compilation

* added test cases and docs on ReSettings

* added ruleparameter.create method

* updated CHANGELOG
2024-01-12 18:32:53 +05:30
Abbas Cyclewala 9bcf4f334b
Users/abbasc52/doc updates (#555)
* added custom type injection in docs

* updated expression parser to support null Resettings and added standalone evaluator sample

* fixed missing bracket
2023-11-26 14:38:11 +05:30
Abbas Cyclewala b783602fe6
updated dependencies (#554)
* updated dependencies

* updated changelog

* added test case to check both static and not static fields are accessible

* fixed warnings
2023-11-26 12:09:17 +05:30
Ayhan Doslu 178248b5a9
Update README.md (#536)
Fixed LamdaExpression typo, changed to LambdaExpression.
2023-10-21 12:42:15 +05:30
Abbas Cyclewala 9e7918dde2
Abbasc52/linux behaviour test (#512)
* Added testcase as per bug

* fixed test case

* Added tests from bugs

* fixed version for FastExpressionCompiler

* updated changelog and added readme file in nuget
2023-08-16 08:53:08 +05:30
dependabot[bot] b9e1ece812
Bump BenchmarkDotNet from 0.13.5 to 0.13.6 (#503)
Bumps [BenchmarkDotNet](https://github.com/dotnet/BenchmarkDotNet) from 0.13.5 to 0.13.6.
- [Release notes](https://github.com/dotnet/BenchmarkDotNet/releases)
- [Commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.5...v0.13.6)

---
updated-dependencies:
- dependency-name: BenchmarkDotNet
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Abbas Cyclewala <abcy@microsoft.com>
2023-07-31 18:23:04 +05:30
dependabot[bot] 00a73138b3
Bump coverallsapp/github-action from 1.1.3 to 2.2.1 (#504)
Bumps [coverallsapp/github-action](https://github.com/coverallsapp/github-action) from 1.1.3 to 2.2.1.
- [Release notes](https://github.com/coverallsapp/github-action/releases)
- [Upgrade guide](https://github.com/coverallsapp/github-action/blob/main/UPGRADE.md)
- [Commits](https://github.com/coverallsapp/github-action/compare/1.1.3...v2.2.1)

---
updated-dependencies:
- dependency-name: coverallsapp/github-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-26 22:03:20 +05:30
Abbas Cyclewala 103e817431
Added option to disable auto type registry (#501)
* - added option to disable auto type registry
- removed caching from RuleExpressionParser

* updated test cases

* added test cases
2023-07-12 18:18:11 +05:30
dependabot[bot] fe70cdad88
Bump dotnet-reportgenerator-globaltool from 5.1.10 to 5.1.23 (#498)
Bumps [dotnet-reportgenerator-globaltool](https://github.com/danielpalme/ReportGenerator) from 5.1.10 to 5.1.23.
- [Release notes](https://github.com/danielpalme/ReportGenerator/releases)
- [Commits](https://github.com/danielpalme/ReportGenerator/compare/v5.1.10...v5.1.23)

---
updated-dependencies:
- dependency-name: dotnet-reportgenerator-globaltool
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-12 11:06:52 +05:30
Abbas Cyclewala 47e7809591
Users/abcy/libupdate (#496)
* updated dependencies

* - Improved global param compilation for multiple rules

* update version number

* updated the changelog
2023-07-08 15:35:01 +05:30
Abbas Cyclewala caf41e3cd3
added basic tests for jobject based inputs (#454)
* added basic tests for jobject based inputs

* fixed test case
2023-02-01 13:23:18 +05:30
dependabot[bot] 7b4e45d2ad
Bump Microsoft.NET.Test.Sdk from 17.3.1 to 17.3.2 (#397)
Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.3.1 to 17.3.2.
- [Release notes](https://github.com/microsoft/vstest/releases)
- [Commits](https://github.com/microsoft/vstest/compare/v17.3.1...v17.3.2)

---
updated-dependencies:
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-14 09:46:56 +00:00
dependabot[bot] ef72cd0f60
Bump Newtonsoft.Json from 13.0.1 to 13.0.2 in /src/RulesEngine (#440)
Bumps [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json) from 13.0.1 to 13.0.2.
- [Release notes](https://github.com/JamesNK/Newtonsoft.Json/releases)
- [Commits](https://github.com/JamesNK/Newtonsoft.Json/compare/13.0.1...13.0.2)

---
updated-dependencies:
- dependency-name: Newtonsoft.Json
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-09 12:45:38 +05:30
dependabot[bot] a2f44b8806
Bump System.Linq.Dynamic.Core from 1.2.18 to 1.2.22 (#423)
Bumps [System.Linq.Dynamic.Core](https://github.com/zzzprojects/System.Linq.Dynamic.Core) from 1.2.18 to 1.2.22.
- [Release notes](https://github.com/zzzprojects/System.Linq.Dynamic.Core/releases)
- [Changelog](https://github.com/zzzprojects/System.Linq.Dynamic.Core/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zzzprojects/System.Linq.Dynamic.Core/compare/v1.2.18...v1.2.22)

---
updated-dependencies:
- dependency-name: System.Linq.Dynamic.Core
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-14 11:07:42 +05:30
XiaoFei Du a30247931d
Fix code commenting. (#404)
Co-authored-by: XiaoFeiDu <romanchaos@163.com>
2022-10-13 09:27:31 +00:00
dependabot[bot] 7949a8b31d
Bump actions/setup-dotnet from 2 to 3 (#396)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 2 to 3.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Abbas Cyclewala <abcy@microsoft.com>
2022-10-06 05:36:30 +00:00
Abbas Cyclewala c5801e6ac6
Update index.md 2022-10-06 10:54:31 +05:30
Abbas Cyclewala f785c15f03
Update README.md 2022-10-06 10:52:00 +05:30
Abbas Cyclewala a76601b412
Update dotnetcore-build.yml 2022-10-02 20:01:51 +05:30
Abbas Cyclewala 48e22a03cf
Update CHANGELOG.md 2022-10-02 19:54:28 +05:30
Abbas Cyclewala d0133155d6
* Updated dependecies (#394)
* * Updated dependecies
* fixed build error
* removed obsolete code

* fixed coverage threshold

* updated changelog

* removed key hashing as it can lead to conflicts

* updated container image
2022-10-01 10:23:01 +05:30
Steven Frew a74f73a44e
suggested fix for issue #384 (#386)
* fix for ruleparameter name changes against cached rules

* fixed indentation to match rest of code

* suggested change implemented
2022-09-05 09:14:18 +05:30
Alex Reich 77750ae0b4
Ef migration fix (#377)
* conventional progress

* Revert "conventional progress"

This reverts commit 9b404416b9.

* Shadow state fixes

* Code Cleanup

Co-authored-by: Alex Reich <Alex_Reich@mechanicsbank.com>
Co-authored-by: areich <alex@alexreich.com>
2022-07-29 09:52:20 +05:30
Kulshekhar Kabra a0c424e742
chore: fix spelling (#360) 2022-06-08 13:46:05 +05:30
dependabot[bot] dd9f6f3979
Bump System.Text.Json from 6.0.3 to 6.0.4 (#359)
Bumps [System.Text.Json](https://github.com/dotnet/runtime) from 6.0.3 to 6.0.4.
- [Release notes](https://github.com/dotnet/runtime/releases)
- [Commits](https://github.com/dotnet/runtime/compare/v6.0.3...v6.0.4)

---
updated-dependencies:
- dependency-name: System.Text.Json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-19 19:48:57 +05:30
dependabot[bot] 840bcbc92b
Bump xunit.runner.visualstudio from 2.4.3 to 2.4.5 (#355)
Bumps [xunit.runner.visualstudio](https://github.com/xunit/visualstudio.xunit) from 2.4.3 to 2.4.5.
- [Release notes](https://github.com/xunit/visualstudio.xunit/releases)
- [Commits](https://github.com/xunit/visualstudio.xunit/commits)

---
updated-dependencies:
- dependency-name: xunit.runner.visualstudio
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Abbas Cyclewala <abcy@microsoft.com>
2022-05-18 15:32:21 +05:30
microsoft-github-policy-service[bot] ad46582151
Microsoft mandatory file (#356)
Co-authored-by: microsoft-github-policy-service[bot] <77245923+microsoft-github-policy-service[bot]@users.noreply.github.com>
2022-05-13 12:56:29 +05:30
dependabot[bot] 1503221cd4
Bump FluentValidation from 10.4.0 to 11.0.1 (#352)
Bumps [FluentValidation](https://github.com/JeremySkinner/fluentvalidation) from 10.4.0 to 11.0.1.
- [Release notes](https://github.com/JeremySkinner/fluentvalidation/releases)
- [Changelog](https://github.com/FluentValidation/FluentValidation/blob/main/Changelog.txt)
- [Commits](https://github.com/JeremySkinner/fluentvalidation/compare/10.4.0...11.0.1)

---
updated-dependencies:
- dependency-name: FluentValidation
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-11 20:51:04 +05:30
dependabot[bot] 9d69423596
Bump github/codeql-action from 1 to 2 (#348)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 1 to 2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-28 21:53:16 +05:30
Abbas Cyclewala 712b39256c
Update RulesEngine.csproj 2022-04-25 10:44:48 +05:30
James Cooper d7ba03040d
Update RulesEngineContext.cs (#347)
This commit adds a line to RulesEngineContext.cs to make Entity Framework Core ignore WorkflowRulesToInject as well as WorkflowsToInject, which resolves an error causing the demo program to crash.  This fixes issue #346.
2022-04-22 06:45:57 +05:30
dependabot[bot] 083a42489b
Bump actions/checkout from 2 to 3 (#324)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-13 21:26:22 +05:30
dependabot[bot] b8cc8cbbb1
Bump System.Text.Json from 6.0.2 to 6.0.3 (#342)
Bumps [System.Text.Json](https://github.com/dotnet/runtime) from 6.0.2 to 6.0.3.
- [Release notes](https://github.com/dotnet/runtime/releases)
- [Commits](https://github.com/dotnet/runtime/compare/v6.0.2...v6.0.3)

---
updated-dependencies:
- dependency-name: System.Text.Json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-13 20:30:39 +05:30
Abbas Cyclewala 571490455c
Abbasc52/combined fixes (#341)
* Fixed actions not working with complex objects

* removed Microsoft.Extensions.Caching dependency

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

* added test case for jsonElement getProperty method

* updated test cases to cover case insensitivity

* updated workflow schema

* added schema for list of workflows

* fixed schema name and added to solution

* Update dotnetcore-build.yml

* Update dotnetcore-build.yml

* updated unit test proj to point to dotnet 6

* removed ilogger
2022-04-12 19:24:16 +05:30
Charlie King 99bad9ffff
In the README.md, there is an example of one creating a worklow and sending it to the rules engine. This MUST be an array (not a single workflow) and the current documentation if one was following along would not compile. I updated the sample code to have a new var List<Workflow> workflows which is passed to the rules engine. The sample 'Example Workflow' is the only workflow in this list of workflows. (#334)
Co-authored-by: Charlie King <charlie.king@dimensional.com>
2022-03-18 14:49:09 +05:30
Abbas Cyclewala 4a2b345fe9
updated package version and documentation update (#289) 2021-12-11 17:09:07 +05:30
Abbas Cyclewala fa9c512c49 Set theme jekyll-theme-cayman 2021-12-11 10:53:52 +05:30
Abbas Cyclewala 60b6561f27
Merge docs to main (#288)
* added more docs for local params and actions

* updated text

* fixed typo

* added wiki link in doc

* added information on expression language

* fixed links
2021-12-11 10:52:36 +05:30
Anton Kheystver 53213bf13e
Add ContainsWorkflow method to check if workflow exists (#283)
* Add ContainsWorkflow method to check if workflow exists

* Add method to interface

Co-authored-by: anton.kheystver <anton.kheystver@opensoftdev.ru>
2021-11-25 21:02:21 +05:30
Abbas Cyclewala 108fa91968
Enhancements (#282)
* * fixed workflowInjection not working
* added optional Inputs filter for EvaluateRuleAction

* * ActionContext now supports optional inputs
* Added support to pass additionalInputs to EvaluateRule Action
2021-11-23 17:47:10 +05:30
dependabot[bot] e6624621df
Bump System.Linq.Dynamic.Core from 1.2.13 to 1.2.14 (#280)
Bumps [System.Linq.Dynamic.Core](https://github.com/zzzprojects/System.Linq.Dynamic.Core) from 1.2.13 to 1.2.14.
- [Release notes](https://github.com/zzzprojects/System.Linq.Dynamic.Core/releases)
- [Changelog](https://github.com/zzzprojects/System.Linq.Dynamic.Core/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zzzprojects/System.Linq.Dynamic.Core/compare/v1.2.13...1.2.14)

---
updated-dependencies:
- dependency-name: System.Linq.Dynamic.Core
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Abbas Cyclewala <abcy@microsoft.com>
2021-11-17 19:05:16 +00:00
dependabot[bot] 4afdce6c49
Bump Microsoft.NET.Test.Sdk from 16.11.0 to 17.0.0 (#266)
Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 16.11.0 to 17.0.0.
- [Release notes](https://github.com/microsoft/vstest/releases)
- [Commits](https://github.com/microsoft/vstest/compare/v16.11.0...v17.0.0)

---
updated-dependencies:
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-18 00:34:02 +05:30
Selman DADAK bc1b70d1cf
edited broken links in Getting-Started.md (#255)
* edited broken links in Getting-Started.md

* edited broken block diagram link.

* edited broken how it works link
2021-10-19 12:04:23 +05:30
Selman DADAK e8ba36cce5
Update Getting-Started.md (#252)
link and class name edited for "WorkflowRules class is deprecated. Use Workflow class instead"
2021-10-13 03:37:46 +00:00
dependabot[bot] 7a90b63b22
Bump System.Linq.Dynamic.Core from 1.2.12 to 1.2.13 (#251)
Bumps [System.Linq.Dynamic.Core](https://github.com/zzzprojects/System.Linq.Dynamic.Core) from 1.2.12 to 1.2.13.
- [Release notes](https://github.com/zzzprojects/System.Linq.Dynamic.Core/releases)
- [Changelog](https://github.com/zzzprojects/System.Linq.Dynamic.Core/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zzzprojects/System.Linq.Dynamic.Core/compare/v1.2.12...v1.2.13)

---
updated-dependencies:
- dependency-name: System.Linq.Dynamic.Core
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-12 15:22:38 +05:30
bavardha 12de7a1437
Updated spelling (#250) 2021-10-11 12:28:14 +05:30
Abbas Cyclewala 6138b9c749
Abbasc52/version update (#246)
* version update

* updated changelog and readme
2021-10-01 15:30:34 +05:30
Alex Reich fb2e19da7c
Rules engine editor readme (#237)
* conventional progress

* Revert "conventional progress"

This reverts commit 9b404416b9.

* RulesEngineEditor inclusion in README

* Moved Editor into 3rd Party Tools

Co-authored-by: Alex Reich <Alex_Reich@mechanicsbank.com>
2021-09-17 09:54:50 +00:00
Abbas Cyclewala 10df53da8a
updated dependabot config (#240) 2021-09-15 19:42:01 +05:30
Alex Reich 2af72285bc
Lang updates (#235)
* conventional progress

* Revert "conventional progress"

This reverts commit 9b404416b9.

* Spelling fixes, request for author inclusion

* Update schema & readme to match new naming convention

Co-authored-by: Alex Reich <Alex_Reich@mechanicsbank.com>
2021-09-15 09:47:46 +05:30
Abbas Cyclewala 1449d05810
Added strongname signing for RulesEngine (#231)
* Added strongname signing
2021-09-09 16:03:52 +05:30
Alex Reich 7b089a8260
Nested rules null operator check (#230)
* conventional progress

* Revert "conventional progress"

This reverts commit 9b404416b9.

* Check for issue on Rules field with missing (null) operator

* Rule Validator cleanup + Rule Validation Tests

Co-authored-by: Alex Reich <Alex_Reich@mechanicsbank.com>
2021-09-07 03:58:23 +00:00
Jason Finch 548faba2a8
Update readme (#228)
Minor sentence structure changes to read easier.
2021-09-06 10:35:37 +05:30
dependabot[bot] be8a91d651
Bump Microsoft.NET.Test.Sdk from 16.10.0 to 16.11.0 (#208)
Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 16.10.0 to 16.11.0.
- [Release notes](https://github.com/microsoft/vstest/releases)
- [Commits](https://github.com/microsoft/vstest/compare/v16.10.0...v16.11.0)

---
updated-dependencies:
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-18 12:27:41 +05:30
Alex Reich 34f77ed2ec
Make consistent singlar/plural names of instances to avoid downstream… (#193)
* Make consistent singlar/plural names of instances to avoid downstream ambiguous reference issues

* Reverted ReSettings pluralization

* conventional progress

* Overhaul to simplify, cleanup, deprecate pr

* Update README.md

* Reverted RuleActions

* Cleanup

* Cleanup

* Cleanup

* Cleanup

* Deprecate RuleAction to plural

* Reverted some name changes per @abbasc52

* Clarity + VS.NET 2019 compilation fix

* Camelcase fixes

* Reverted Id properties

* Documentation updates

Co-authored-by: Alex Reich <Alex_Reich@mechanicsbank.com>
Co-authored-by: Alex Reich <alex@alexreich.com>
Co-authored-by: Abbas Cyclewala <abcy@microsoft.com>
2021-08-13 10:04:47 +05:30
Aleksandar Ivanov fe38ed5c9f
Resolves: Add GitHub Codespaces configuration (#167)
* Create Codespaces configuration (#10)

* locked image version to latest tagged
2021-08-12 05:30:39 +00:00
dependabot[bot] 3ea7fdac96
Bump FastExpressionCompiler from 3.2.0 to 3.2.1 (#188)
Bumps [FastExpressionCompiler](https://github.com/dadhi/FastExpressionCompiler) from 3.2.0 to 3.2.1.
- [Release notes](https://github.com/dadhi/FastExpressionCompiler/releases)
- [Commits](https://github.com/dadhi/FastExpressionCompiler/compare/v3.2.0...v3.2.1)

---
updated-dependencies:
- dependency-name: FastExpressionCompiler
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Abbas Cyclewala <abcy@microsoft.com>
2021-08-10 16:39:03 +00:00
dependabot[bot] c4d5bbbae4
Bump System.Linq.Dynamic.Core from 1.2.11 to 1.2.12 (#195)
Bumps [System.Linq.Dynamic.Core](https://github.com/zzzprojects/System.Linq.Dynamic.Core) from 1.2.11 to 1.2.12.
- [Release notes](https://github.com/zzzprojects/System.Linq.Dynamic.Core/releases)
- [Changelog](https://github.com/zzzprojects/System.Linq.Dynamic.Core/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zzzprojects/System.Linq.Dynamic.Core/compare/v1.2.11...v1.2.12)

---
updated-dependencies:
- dependency-name: System.Linq.Dynamic.Core
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-10 21:58:55 +05:30
Alex Reich f9def1c6f7
Deprecate Newtonsoft.Json to System.Text.Json (#191)
Co-authored-by: Alex Reich <Alex_Reich@mechanicsbank.com>
2021-07-28 14:03:11 +05:30
Alex Reich 4796cbfd70
README updates related to EF (#190)
Basic Demo shown for code only solution

Co-authored-by: Alex Reich <Alex_Reich@mechanicsbank.com>
2021-07-28 12:43:19 +05:30
Alex Reich 9f898b703b
Entity Framework demonstration (#186)
* Initial Commit

* WIP

* Readability + null ef issue r&d

* Cleanup

* Minimum Sample

* Refactored to remove EF from DemoApp

* Fixed RuleValidator issue
Added EmptyRulesTest for expected simple and nested rules empty exception

Co-authored-by: Alex Reich <alex@alexreich.com>
Co-authored-by: Alex Reich <Alex_Reich@mechanicsbank.com>
2021-07-22 21:59:26 +05:30
85 changed files with 4046 additions and 1792 deletions

View File

@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-reportgenerator-globaltool": {
"version": "4.7.1",
"version": "5.1.23",
"commands": [
"reportgenerator"
]

View File

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

View File

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

View File

@ -35,11 +35,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -50,7 +50,7 @@ jobs:
# 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
@ -64,5 +64,5 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v3

View File

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

View File

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

View File

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

6
.gitignore vendored
View File

@ -330,4 +330,8 @@ ASALocalRun/
.mfractor/
/src/RulesEngine/RulesEngine.sln.licenseheader
/assets/RulesEnginePackageFile.xml
coveragereport/
coveragereport/
src/**/*.snk
dist

View File

@ -2,6 +2,72 @@
All notable changes to this project will be documented in this file.
## [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
## [5.0.2]
- Fixed Scoped Params returning incorrect results in some corner case scenarios
## [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
@ -24,12 +90,12 @@ Breaking Change:
- 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
- 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

View File

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

View File

@ -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,7 +18,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
CHANGELOG.md = CHANGELOG.md
global.json = global.json
README.md = README.md
schema\workflowRules-schema.json = schema\workflowRules-schema.json
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}"
@ -26,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
@ -48,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

41
SECURITY.md Normal file
View File

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

View File

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

View File

@ -2,11 +2,11 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.0" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
<!--<PackageReference Include="RulesEngine" Version="3.0.2" />-->
</ItemGroup>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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="13.0.1" />
<ProjectReference Include="../../src/RulesEngine/RulesEngine.csproj" />
<ProjectReference Include="..\DemoApp.EFDataExample\DemoApp.EFDataExample.csproj" />
</ItemGroup>
<ItemGroup>

73
demo/DemoApp/EFDemo.cs Normal file
View File

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

61
demo/DemoApp/JSONDemo.cs Normal file
View File

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

View File

@ -49,10 +49,10 @@ 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;

View File

@ -8,7 +8,9 @@ namespace DemoApp
public static void Main(string[] args)
{
new BasicDemo().Run();
new JSONDemo().Run();
new NestedInputDemo().Run();
new EFDemo().Run();
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
dotnet tool restore
dotnet reportgenerator "-reports:**/coverage.cobertura.xml" "-targetdir:coveragereport" -reporttypes:"Html;lcov;Cobertura"

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 recursively
/// </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);
}
}
}
}

View File

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

View File

@ -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,13 +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 WorkflowRules[] workflowRules);
void AddOrUpdateWorkflow(params Workflow[] Workflows);
}
}

View File

@ -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>
@ -44,19 +64,26 @@ namespace RulesEngine.Models
/// </summary>
public bool EnableScopedParams { get; set; } = true;
/// <summary>
/// Sets whether expression are case sensitive
/// </summary>
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>
/// Enables Local params for rules
/// Whether to use FastExpressionCompiler for rule compilation
/// </summary>
[Obsolete("Use 'EnableScopedParams' instead. This will be removed in next major version")]
public bool EnableLocalParams {
get { return EnableScopedParams; }
set { EnableScopedParams = value; }
}
public bool UseFastExpressionCompiler { get; set; } = true;
}
public enum NestedRuleExecutionMode

View File

@ -3,7 +3,6 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@ -34,13 +33,9 @@ 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 IEnumerable<string> WorkflowRulesToInject { 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; }

View File

@ -1,10 +1,17 @@
// 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
{

View File

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

View File

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

View File

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

View File

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

View File

@ -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
@ -187,7 +178,7 @@ namespace RulesEngine
return (paramArray) => {
var (isSuccess, resultList) = ApplyOperation(paramArray, ruleFuncList, operation);
Func<object[], bool> isSuccessFn = (p) => isSuccess;
bool isSuccessFn(object[] p) => isSuccess;
var result = Helpers.ToResultTree(_reSettings, parentRule, resultList, isSuccessFn);
return result(paramArray);
};
@ -238,13 +229,19 @@ namespace RulesEngine
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)
{
if(ruleExpParams.Length == 0)
{
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();
@ -263,14 +260,6 @@ namespace RulesEngine
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;
};
}

View File

@ -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,41 +14,39 @@ namespace RulesEngine
internal class RulesCache
{
/// <summary>The compile rules</summary>
private ConcurrentDictionary<string, (IDictionary<string, RuleFunc<RuleResultTree>>, Int64)> _compileRules = new ConcurrentDictionary<string, (IDictionary<string, RuleFunc<RuleResultTree>>, Int64)>();
private readonly MemCache _compileRules;
/// <summary>The workflow rules</summary>
private ConcurrentDictionary<string, (WorkflowRules, Int64)> _workflowRules = new ConcurrentDictionary<string, (WorkflowRules, Int64)>();
private readonly ConcurrentDictionary<string, (Workflow, long)> _workflow = new ConcurrentDictionary<string, (Workflow, long)>();
public RulesCache(ReSettings reSettings)
{
_compileRules = new MemCache(reSettings.CacheConfig);
}
/// <summary>Determines whether [contains workflow rules] [the specified workflow name].</summary>
/// <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)
{
Int64 ticks = DateTime.UtcNow.Ticks;
_workflowRules.AddOrUpdate(workflowName, (rules, ticks), (k, v) => (rules, ticks));
long ticks = DateTime.UtcNow.Ticks;
_workflow.AddOrUpdate(workflowName, (rules, ticks), (k, v) => (rules, ticks));
}
/// <summary>Adds the or update compiled rule.</summary>
@ -55,8 +54,8 @@ namespace RulesEngine
/// <param name="compiledRule">The compiled rule.</param>
public void AddOrUpdateCompiledRule(string compiledRuleKey, IDictionary<string, RuleFunc<RuleResultTree>> compiledRule)
{
Int64 ticks = DateTime.UtcNow.Ticks;
_compileRules.AddOrUpdate(compiledRuleKey, (compiledRule, ticks), (k, v) => (compiledRule, ticks));
long ticks = DateTime.UtcNow.Ticks;
_compileRules.Set(compiledRuleKey,(compiledRule, ticks));
}
/// <summary>Checks if the compiled rules are up-to-date.</summary>
@ -66,11 +65,11 @@ namespace RulesEngine
/// <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, Int64 tick) compiledRulesObj))
if (_compileRules.TryGetValue(compiledRuleKey, out (IDictionary<string, RuleFunc<RuleResultTree>> rules, long tick) compiledRulesObj))
{
if (_workflowRules.TryGetValue(workflowName, out (WorkflowRules rules, Int64 tick) workflowRulesObj))
if (_workflow.TryGetValue(workflowName, out (Workflow rules, long tick) WorkflowsObj))
{
return compiledRulesObj.tick >= workflowRulesObj.tick;
return compiledRulesObj.tick >= WorkflowsObj.tick;
}
}
@ -80,38 +79,38 @@ namespace RulesEngine
/// <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)
{
if (_workflowRules.TryGetValue(workflowName, out (WorkflowRules rules, Int64 tick) workflowRulesObj))
if (_workflow.TryGetValue(workflowName, out (Workflow rules, long tick) WorkflowsObj))
{
var workflowRules = workflowRulesObj.rules;
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
{
@ -125,19 +124,19 @@ namespace RulesEngine
/// <returns>CompiledRule.</returns>
public IDictionary<string, RuleFunc<RuleResultTree>> GetCompiledRules(string compiledRulesKey)
{
return _compileRules[compiledRulesKey].Item1;
return _compileRules.Get<(IDictionary<string, RuleFunc<RuleResultTree>> rules, long tick)>(compiledRulesKey).rules;
}
/// <summary>Removes the specified workflow name.</summary>
/// <param name="workflowName">Name of the workflow.</param>
public void Remove(string workflowName)
{
if (_workflowRules.TryRemove(workflowName, out (WorkflowRules, Int64) workflowObj))
if (_workflow.TryRemove(workflowName, out var workflowObj))
{
var compiledKeysToRemove = _compileRules.Keys.Where(key => key.StartsWith(workflowName));
var compiledKeysToRemove = _compileRules.GetKeys().Where(key => key.StartsWith(workflowName));
foreach (var key in compiledKeysToRemove)
{
_compileRules.TryRemove(key, out (IDictionary<string, RuleFunc<RuleResultTree>>, Int64) val);
_compileRules.Remove(key);
}
}
}

View File

@ -1,421 +1,444 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using FluentValidation;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using RulesEngine.Actions;
using RulesEngine.Exceptions;
using RulesEngine.ExpressionBuilders;
using RulesEngine.Interfaces;
using RulesEngine.Models;
using RulesEngine.Validators;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace RulesEngine
{
/// <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);
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="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);
if (!_rulesCache.ContainsWorkflowRules(workflowRule.WorkflowName))
{
_rulesCache.AddOrUpdateWorkflowRules(workflowRule.WorkflowName, workflowRule);
}
else
{
throw new ValidationException($"Cannot add workflow `{workflowRule.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="workflowRules">The workflow rules.</param>
/// <exception cref="RuleValidationException"></exception>
public void AddOrUpdateWorkflow(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.AreCompiledRulesUpToDate(compileRulesKey, workflowName))
{
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
}
}

View File

@ -1,15 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Version>3.3.0</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,23 +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="3.2.0" />
<PackageReference Include="FluentValidation" Version="10.3.0" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="System.Linq" Version="4.3.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.2.11" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
<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>

View File

@ -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,7 +39,7 @@ 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);
});
}

View File

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

View File

@ -18,8 +18,8 @@ namespace RulesEngine.UnitTest.ActionTests
[Fact]
public async Task CustomActionOnRuleMustHaveContextValues()
{
var workflows = GetWorkflowRules();
var re = new RulesEngine(workflows, null, reSettings: new ReSettings {
var workflow = GetWorkflow();
var re = new RulesEngine(workflow, reSettings: new ReSettings {
CustomActions = new Dictionary<string, System.Func<Actions.ActionBase>> {
{ "ReturnContext", () => new ReturnContextAction() }
@ -33,13 +33,13 @@ namespace RulesEngine.UnitTest.ActionTests
[Fact]
public async Task CustomAction_WithSystemTextJsobOnRuleMustHaveContextValues()
{
var workflows = GetWorkflowRules();
var workflowStr = JsonConvert.SerializeObject(workflows);
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<WorkflowRules[]>(workflowStr,serializationOptions);
var workflowViaTextJson = System.Text.Json.JsonSerializer.Deserialize<Workflow[]>(workflowStr,serializationOptions);
var re = new RulesEngine(workflows, null, reSettings: new ReSettings {
var re = new RulesEngine(workflow, reSettings: new ReSettings {
CustomActions = new Dictionary<string, System.Func<Actions.ActionBase>> {
{ "ReturnContext", () => new ReturnContextAction() }
@ -51,10 +51,10 @@ namespace RulesEngine.UnitTest.ActionTests
var result = await re.ExecuteAllRulesAsync("successReturnContextAction", true);
}
private WorkflowRules[] GetWorkflowRules()
private Workflow[] GetWorkflow()
{
return new WorkflowRules[] {
new WorkflowRules {
return new Workflow[] {
new Workflow {
WorkflowName = "successReturnContextAction",
Rules = new Rule[] {
new Rule {

View File

@ -5,11 +5,13 @@ 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)

View File

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

View File

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

View File

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

View File

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

View File

@ -22,9 +22,9 @@ namespace RulesEngine.UnitTest
[InlineData(NestedRuleExecutionMode.Performance)]
public async Task NestedRulesShouldFollowExecutionMode(NestedRuleExecutionMode mode)
{
var workflows = GetWorkflows();
var workflow = GetWorkflow();
var reSettings = new ReSettings { NestedRuleExecutionMode = mode };
var rulesEngine = new RulesEngine(workflows, reSettings: reSettings);
var rulesEngine = new RulesEngine(workflow, reSettings: reSettings);
dynamic input1 = new ExpandoObject();
input1.trueValue = true;
@ -68,9 +68,9 @@ namespace RulesEngine.UnitTest
[Fact]
private async Task NestedRulesWithNestedActions_ReturnsCorrectResults()
{
var workflows = GetWorkflows();
var workflow = GetWorkflow();
var reSettings = new ReSettings { };
var rulesEngine = new RulesEngine(workflows, reSettings: reSettings);
var rulesEngine = new RulesEngine(workflow, reSettings: reSettings);
dynamic input1 = new ExpandoObject();
input1.trueValue = true;
@ -84,16 +84,16 @@ namespace RulesEngine.UnitTest
[Fact]
private async Task NestedRulesWithNestedActions_WorkflowParsedWithSystemTextJson_ReturnsCorrectResults()
{
var workflows = GetWorkflows();
var workflowStr = JsonConvert.SerializeObject(workflows);
var workflow = GetWorkflow();
var workflowStr = JsonConvert.SerializeObject(workflow);
var serializationOptions = new System.Text.Json.JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } };
var workflowsViaTextJson = System.Text.Json.JsonSerializer.Deserialize<WorkflowRules[]>(workflowStr, serializationOptions);
var workflowViaTextJson = System.Text.Json.JsonSerializer.Deserialize<Workflow[]>(workflowStr, serializationOptions);
var reSettings = new ReSettings { };
var rulesEngine = new RulesEngine(workflowsViaTextJson, reSettings: reSettings);
var rulesEngine = new RulesEngine(workflowViaTextJson, reSettings: reSettings);
dynamic input1 = new ExpandoObject();
input1.trueValue = true;
@ -108,10 +108,10 @@ namespace RulesEngine.UnitTest
private WorkflowRules[] GetWorkflows()
private Workflow[] GetWorkflow()
{
return new[] {
new WorkflowRules {
new Workflow {
WorkflowName = "NestedRulesTest",
Rules = new Rule[] {
new Rule {
@ -176,7 +176,7 @@ namespace RulesEngine.UnitTest
}
}
},
new WorkflowRules {
new Workflow {
WorkflowName = "NestedRulesActionsTest",
Rules = new Rule[] {
new Rule {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, reSettings: new ReSettings() { EnableExceptionAsErrorMessage = false });
var workflow = GetWorkflows();
var rulesEngine = new RulesEngine(workflow, reSettings: new ReSettings() { EnableExceptionAsErrorMessage = false });
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 {

View File

@ -1,18 +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.17.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="System.Text.Json" Version="5.0.2" />
<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.1.0">
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@ -24,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>
@ -45,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>

View File

@ -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);
@ -75,12 +84,12 @@ namespace RulesEngine.UnitTest
[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,
@ -101,12 +110,13 @@ namespace RulesEngine.UnitTest
[Theory]
[InlineData("GlobalParamsOnly")]
[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);
@ -123,10 +133,10 @@ namespace RulesEngine.UnitTest
[InlineData("LocalParamsOnlyWithComplexInput")]
public async Task RuntimeErrorInScopedParam_ShouldAppearAsErrorMessage(string workflowName)
{
var workflows = GetWorkflowRulesList();
var workflow = GetWorkflowList();
var engine = new RulesEngine(new string[] { }, null);
engine.AddWorkflow(workflows);
engine.AddWorkflow(workflow);
@ -141,9 +151,37 @@ namespace RulesEngine.UnitTest
}
[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>());
@ -162,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)
@ -176,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 {
@ -188,7 +222,7 @@ namespace RulesEngine.UnitTest
}
}
},
new WorkflowRules {
new Workflow {
WorkflowName = "LocalParamsOnly",
Rules = new List<Rule> {
new Rule {
@ -209,7 +243,7 @@ namespace RulesEngine.UnitTest
},
}
},
new WorkflowRules {
new Workflow {
WorkflowName = "LocalParamsOnly2",
Rules = new List<Rule> {
new Rule {
@ -226,7 +260,7 @@ namespace RulesEngine.UnitTest
}
},
new WorkflowRules {
new Workflow {
WorkflowName = "GlobalParamsOnly",
GlobalParams = new List<ScopedParam> {
new ScopedParam {
@ -241,7 +275,7 @@ namespace RulesEngine.UnitTest
}
}
},
new WorkflowRules {
new Workflow {
WorkflowName = "GlobalAndLocalParams",
GlobalParams = new List<ScopedParam> {
new ScopedParam {
@ -263,7 +297,7 @@ namespace RulesEngine.UnitTest
}
},
new WorkflowRules {
new Workflow {
WorkflowName = "GlobalParamReferencedInLocalParams",
GlobalParams = new List<ScopedParam> {
new ScopedParam {
@ -285,7 +319,7 @@ namespace RulesEngine.UnitTest
},
}
},
new WorkflowRules {
new Workflow {
WorkflowName = "GlobalParamReferencedInNextGlobalParams",
GlobalParams = new List<ScopedParam> {
new ScopedParam {
@ -304,7 +338,7 @@ namespace RulesEngine.UnitTest
},
}
},
new WorkflowRules {
new Workflow {
WorkflowName = "LocalParamReferencedInNextLocalParams",
Rules = new List<Rule> {
new Rule {
@ -323,7 +357,7 @@ namespace RulesEngine.UnitTest
},
}
},
new WorkflowRules {
new Workflow {
WorkflowName = "GlobalParamAndLocalParamsInNestedRules",
GlobalParams = new List<ScopedParam> {
new ScopedParam {
@ -362,7 +396,7 @@ namespace RulesEngine.UnitTest
}
}
},
new WorkflowRules {
new Workflow {
WorkflowName = "LocalParamsOnlyWithComplexInput",
Rules = new List<Rule> {
new Rule {
@ -378,7 +412,7 @@ namespace RulesEngine.UnitTest
}
}
},
new WorkflowRules {
new Workflow {
WorkflowName = "GlobalParamsOnlyWithComplexInput",
GlobalParams = new List<ScopedParam> {
new ScopedParam {
@ -390,9 +424,39 @@ namespace RulesEngine.UnitTest
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
}
}
}
};
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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