├── .config └── dotnet-tools.json ├── .devcontainer └── devcontainer.json ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ └── dotnetcore-build.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── RulesEngine.sln ├── SECURITY.md ├── assets └── BlockDiagram.png ├── benchmark └── RulesEngineBenchmark │ ├── Program.cs │ ├── RulesEngineBenchmark.csproj │ └── Workflows │ ├── Discount.json │ └── NestedInputDemo.json ├── demo ├── DemoApp.EFDataExample │ ├── DemoApp.EFDataExample.csproj │ ├── RulesEngineContext.cs │ └── RulesEngineDemoContext.cs └── DemoApp │ ├── BasicDemo.cs │ ├── DemoApp.csproj │ ├── EFDemo.cs │ ├── JSONDemo.cs │ ├── NestedInputDemo.cs │ ├── Program.cs │ └── Workflows │ ├── Discount.json │ └── NestedInputDemo.json ├── deployment └── build-signed.ps1 ├── docs ├── Getting-Started.md ├── Home.md ├── Introduction.md ├── Use-Case.md ├── _Sidebar.md ├── _config.yml └── index.md ├── global.json ├── schema ├── workflow-list-schema.json └── workflow-schema.json ├── scripts ├── check-coverage.ps1 └── generate-coverage-report.ps1 ├── signing └── RulesEngine-publicKey.snk ├── src └── RulesEngine │ ├── Actions │ ├── ActionBase.cs │ ├── ActionContext.cs │ ├── ActionFactory.cs │ ├── EvaluateRuleAction.cs │ └── ExpressionOutputAction.cs │ ├── CustomTypeProvider.cs │ ├── Exceptions │ ├── ExpressionParserException.cs │ ├── RuleException.cs │ ├── RuleValidationException.cs │ └── ScopedParamException.cs │ ├── ExpressionBuilders │ ├── LambdaExpressionBuilder.cs │ ├── RuleExpressionBuilderBase.cs │ └── RuleExpressionParser.cs │ ├── Extensions │ ├── EnumerableExtensions.cs │ └── ListofRuleResultTreeExtension.cs │ ├── HelperFunctions │ ├── Constants.cs │ ├── ExpressionUtils.cs │ ├── Helpers.cs │ ├── MemCache.cs │ └── Utils.cs │ ├── Interfaces │ └── IRulesEngine.cs │ ├── Models │ ├── ActionInfo.cs │ ├── ActionResult.cs │ ├── ActionRuleResult.cs │ ├── ReSettings.cs │ ├── Rule.cs │ ├── RuleActions.cs │ ├── RuleDelegate.cs │ ├── RuleErrorType.cs │ ├── RuleExpressionParameter.cs │ ├── RuleExpressionType.cs │ ├── RuleParameter.cs │ ├── RuleResultTree.cs │ ├── ScopedParam.cs │ └── Workflow.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── RuleCompiler.cs │ ├── RuleExpressionBuilderFactory.cs │ ├── RulesCache.cs │ ├── RulesEngine.cs │ ├── RulesEngine.csproj │ └── Validators │ ├── RuleValidator.cs │ └── WorkflowRulesValidator.cs └── test └── RulesEngine.UnitTest ├── ActionTests ├── ActionContextTests.cs ├── CustomActionTest.cs ├── MockClass │ └── ReturnContextAction.cs └── RulesEngineWithActionsTests.cs ├── BusinessRuleEngineTest.cs ├── CaseSensitiveTests.cs ├── CustomTypeProviderTests.cs ├── EmptyRulesTest.cs ├── ExpressionUtilsTest.cs ├── LambdaExpressionBuilderTest.cs ├── ListofRuleResultTreeExtensionTest.cs ├── NestedRulesTest.cs ├── ParameterNameChangeTest.cs ├── RuleCompilerTest.cs ├── RuleExpressionBuilderFactoryTest.cs ├── RuleExpressionParserTests └── RuleExpressionParserTests.cs ├── RuleParameterTests.cs ├── RuleTestClass.cs ├── RuleValidationTest.cs ├── RulesEnabledTests.cs ├── RulesEngine.UnitTest.csproj ├── ScopedParamsTest.cs ├── TestData ├── rules1.json ├── rules10.json ├── rules11.json ├── rules2.json ├── rules3.json ├── rules4.json ├── rules5.json ├── rules6.json ├── rules7.json ├── rules8.json └── rules9.json ├── TypedClassTests.cs └── UtilsTests.cs /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-reportgenerator-globaltool": { 6 | "version": "5.1.26", 7 | "commands": [ 8 | "reportgenerator" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RulesEngine Codespace", 3 | "image": "mcr.microsoft.com/vscode/devcontainers/dotnet:0-6.0", 4 | "settings": { 5 | "terminal.integrated.defaultProfile.linux": "bash" 6 | }, 7 | "extensions": [ 8 | "eamodio.gitlens", 9 | "ms-dotnettools.csharp", 10 | "VisualStudioExptTeam.vscodeintellicode", 11 | "ms-vscode.powershell", 12 | "cschleiden.vscode-github-actions", 13 | "redhat.vscode-yaml", 14 | "bierner.markdown-preview-github-styles", 15 | "coenraads.bracket-pair-colorizer", 16 | "vscode-icons-team.vscode-icons", 17 | "editorconfig.editorconfig", 18 | "aliasadidev.nugetpackagemanagergui", 19 | "formulahendry.dotnet-test-explorer" 20 | ], 21 | "postCreateCommand": "dotnet restore RulesEngine.sln && dotnet build RulesEngine.sln --configuration Release --no-restore && dotnet test RulesEngine.sln --configuration Release --no-build --verbosity minimal", 22 | "features": { 23 | "powershell": "7.1" 24 | }, 25 | } 26 | // Built with ❤ by [Pipeline Foundation](https://pipeline.foundation) 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Rules in this file were initially inferred by Visual Studio IntelliCode from the C:\Users\purunjaybhal\source\repos\RulesEngine codebase based on best match to current usage at 22-12-2020 2 | # You can modify the rules from these initially generated values to suit your own policies 3 | # You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference 4 | [*.cs] 5 | 6 | 7 | #Core editorconfig formatting - indentation 8 | 9 | #use soft tabs (spaces) for indentation 10 | indent_style = space 11 | 12 | #Formatting - indentation options 13 | 14 | #indent switch case contents. 15 | csharp_indent_case_contents = true 16 | #indent switch labels 17 | csharp_indent_switch_labels = true 18 | 19 | #Formatting - new line options 20 | 21 | #place catch statements on a new line 22 | csharp_new_line_before_catch = true 23 | #place else statements on a new line 24 | csharp_new_line_before_else = true 25 | #require members of anonymous types to be on separate lines 26 | csharp_new_line_before_members_in_anonymous_types = true 27 | #require members of object intializers to be on separate lines 28 | csharp_new_line_before_members_in_object_initializers = true 29 | #require braces to be on a new line for methods, control_blocks, and types (also known as "Allman" style) 30 | csharp_new_line_before_open_brace = methods, control_blocks, types 31 | 32 | #Formatting - organize using options 33 | 34 | #do not place System.* using directives before other using directives 35 | dotnet_sort_system_directives_first = false 36 | 37 | #Formatting - spacing options 38 | 39 | #require NO space between a cast and the value 40 | csharp_space_after_cast = false 41 | #require a space before the colon for bases or interfaces in a type declaration 42 | csharp_space_after_colon_in_inheritance_clause = true 43 | #require a space after a keyword in a control flow statement such as a for loop 44 | csharp_space_after_keywords_in_control_flow_statements = true 45 | #require a space before the colon for bases or interfaces in a type declaration 46 | csharp_space_before_colon_in_inheritance_clause = true 47 | #remove space within empty argument list parentheses 48 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 49 | #remove space between method call name and opening parenthesis 50 | csharp_space_between_method_call_name_and_opening_parenthesis = false 51 | #do not place space characters after the opening parenthesis and before the closing parenthesis of a method call 52 | csharp_space_between_method_call_parameter_list_parentheses = false 53 | #remove space within empty parameter list parentheses for a method declaration 54 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 55 | #place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list. 56 | csharp_space_between_method_declaration_parameter_list_parentheses = false 57 | 58 | #Formatting - wrapping options 59 | 60 | #leave code block on single line 61 | csharp_preserve_single_line_blocks = true 62 | #leave statements and member declarations on the same line 63 | csharp_preserve_single_line_statements = true 64 | 65 | #Style - Code block preferences 66 | 67 | #prefer curly braces even for one line of code 68 | csharp_prefer_braces = true:suggestion 69 | 70 | #Style - expression bodied member options 71 | 72 | #prefer block bodies for constructors 73 | csharp_style_expression_bodied_constructors = false:suggestion 74 | #prefer block bodies for methods 75 | csharp_style_expression_bodied_methods = false:suggestion 76 | 77 | #Style - expression level options 78 | 79 | #prefer out variables to be declared inline in the argument list of a method call when possible 80 | csharp_style_inlined_variable_declaration = true:suggestion 81 | #prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them 82 | dotnet_style_predefined_type_for_member_access = true:suggestion 83 | 84 | #Style - Expression-level preferences 85 | 86 | #prefer objects to be initialized using object initializers when possible 87 | dotnet_style_object_initializer = true:suggestion 88 | #prefer inferred anonymous type member names 89 | dotnet_style_prefer_inferred_anonymous_type_member_names = false:suggestion 90 | 91 | #Style - implicit and explicit types 92 | 93 | #prefer var over explicit type in all cases, unless overridden by another code style rule 94 | csharp_style_var_elsewhere = true:suggestion 95 | #prefer var is used to declare variables with built-in system types such as int 96 | csharp_style_var_for_built_in_types = true:suggestion 97 | #prefer var when the type is already mentioned on the right-hand side of a declaration expression 98 | csharp_style_var_when_type_is_apparent = true:suggestion 99 | 100 | #Style - language keyword and framework type options 101 | 102 | #prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them 103 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 104 | 105 | #Style - Miscellaneous preferences 106 | 107 | #prefer anonymous functions over local functions 108 | csharp_style_pattern_local_over_anonymous_function = false:suggestion 109 | 110 | #Style - modifier options 111 | 112 | #prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods. 113 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion 114 | 115 | #Style - Modifier preferences 116 | 117 | #when this rule is set to a list of modifiers, prefer the specified ordering. 118 | csharp_preferred_modifier_order = public,private,internal,readonly,static,async,override,sealed:suggestion 119 | 120 | #Style - qualification options 121 | 122 | #prefer fields not to be prefaced with this. or Me. in Visual Basic 123 | dotnet_style_qualification_for_field = false:suggestion 124 | #prefer methods not to be prefaced with this. or Me. in Visual Basic 125 | dotnet_style_qualification_for_method = false:suggestion 126 | #prefer properties not to be prefaced with this. or Me. in Visual Basic 127 | dotnet_style_qualification_for_property = false:suggestion 128 | 129 | 130 | #file header 131 | [*.{cs,vb}] 132 | file_header_template = Copyright (c) Microsoft Corporation.\nLicensed under the MIT License. 133 | file_header_template_style = prepend:error 134 | file_header_template_style = replace:suggestion 135 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | # default location of `.github/workflows` 5 | directory: "/" 6 | open-pull-requests-limit: 3 7 | schedule: 8 | interval: "weekly" 9 | # assignees: 10 | # - assignee_one 11 | # reviewers: 12 | # - reviewer_one 13 | 14 | - package-ecosystem: "nuget" 15 | # location of package manifests 16 | directory: "/" 17 | open-pull-requests-limit: 3 18 | schedule: 19 | interval: "weekly" 20 | ignore: 21 | - dependency-name: "*" 22 | update-types: ["version-update:semver-minor"] 23 | # assignees: 24 | # - assignee_one 25 | # reviewers: 26 | # - reviewer_one 27 | 28 | # Built with ❤ by [Pipeline Foundation](https://pipeline.foundation) 29 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main, develop ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '22 15 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'csharp' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v4 39 | 40 | - name: Setup .NET Core 41 | uses: actions/setup-dotnet@v4 42 | with: 43 | dotnet-version: | 44 | 6.0.x 45 | 8.0.x 46 | 9.0.x 47 | 48 | # Initializes the CodeQL tools for scanning. 49 | - name: Initialize CodeQL 50 | uses: github/codeql-action/init@v3 51 | with: 52 | languages: ${{ matrix.language }} 53 | # If you wish to specify custom queries, you can do so here or in a config file. 54 | # By default, queries listed here will override any specified in a config file. 55 | # Prefix the list here with "+" to use these queries and those in the config file. 56 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 57 | 58 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 59 | # If this step fails, then you should remove it and run the build manually (see below) 60 | - name: Autobuild 61 | uses: github/codeql-action/autobuild@v3 62 | 63 | # ℹ️ Command-line programs to run using the OS shell. 64 | # 📚 https://git.io/JvXDl 65 | 66 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 67 | # and modify them (or add more) to build your code if your project 68 | # uses a compiled language 69 | 70 | #- run: | 71 | # make bootstrap 72 | # make release 73 | 74 | - name: Perform CodeQL Analysis 75 | uses: github/codeql-action/analyze@v3 76 | 77 | -------------------------------------------------------------------------------- /.github/workflows/dotnetcore-build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main, develop ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Setup .NET Core 15 | uses: actions/setup-dotnet@v4 16 | with: 17 | dotnet-version: | 18 | 6.0.x 19 | 8.0.x 20 | 9.0.x 21 | 22 | - name: Install dependencies 23 | run: dotnet restore RulesEngine.sln 24 | 25 | - name: Build 26 | run: dotnet build RulesEngine.sln --configuration Release --no-restore 27 | 28 | - name: Test 29 | run: dotnet test RulesEngine.sln --collect:"XPlat Code Coverage" --no-build --configuration Release --verbosity m 30 | 31 | - name: Generate Report 32 | shell: pwsh 33 | run: ./scripts/generate-coverage-report.ps1 34 | 35 | - name: Check Coverage 36 | shell: pwsh 37 | run: ./scripts/check-coverage.ps1 -reportPath coveragereport/Cobertura.xml -threshold 94 38 | 39 | - name: Coveralls GitHub Action 40 | uses: coverallsapp/github-action@v2.3.6 41 | if: ${{ github.event_name == 'push' }} 42 | with: 43 | github-token: ${{ secrets.GITHUB_TOKEN }} 44 | path-to-lcov: ./coveragereport/lcov.info 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | /src/RulesEngine/RulesEngine.sln.licenseheader 332 | /assets/RulesEnginePackageFile.xml 333 | coveragereport/ 334 | 335 | src/**/*.snk 336 | 337 | dist -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/demo/DemoApp/bin/Debug/netcoreapp3.1/DemoApp.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/demo/DemoApp", 16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach", 24 | "processId": "${command:pickProcess}" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotnetCoreExplorer.searchpatterns": "test/**/bin/Debug/netcoreapp*/*.{dll,exe,json}", 3 | "coverage-gutters.coverageBaseDir": "coveragereport", 4 | "coverage-gutters.coverageReportFileName": "coveragereport/**/index.html" 5 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/demo/DemoApp/DemoApp.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/demo/DemoApp/DemoApp.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/demo/DemoApp/DemoApp.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [5.0.3] 6 | - Updated dependencies to latest 7 | - Fixed RulesEngine throwing exception when type name is same as input name 8 | - Added config to disable FastCompile for expressions 9 | - Added RuleParameter.Create method for better handling on types when value is null 10 | 11 | ## [5.0.2] 12 | - Fixed Scoped Params returning incorrect results in some corner case scenarios 13 | 14 | ## [5.0.1] 15 | - Added option to disable automatic type registry for input parameters in reSettings 16 | - Added option to make expression case sensitive in reSettings 17 | 18 | ## [5.0.0] 19 | - Fixed security bug related to System.Dynamic.Linq.Core 20 | 21 | ### Breaking Changes 22 | - 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 23 | 24 | 25 | ## [4.0.0] 26 | - RulesEngine is now available in both dotnet 6 and netstandard 2.0 27 | - Dependency on ILogger, MemoryCache have been removed 28 | - Obsolete Properties and Methods have been removed 29 | - Fixed name of RuleParameter is ignored if the type is recognized (by @peeveen) 30 | ### Breaking Changes 31 | - ILogger has been removed from RulesEngine and all its constructors 32 | ```diff 33 | - RulesEngine(string[] jsonConfig, ILogger logger = null, ReSettings reSettings = null) 34 | + RulesEngine(string[] jsonConfig, ReSettings reSettings = null) 35 | 36 | - RulesEngine(Workflow[] Workflows, ILogger logger = null, ReSettings reSettings = null) 37 | + RulesEngine(Workflow[] Workflows, ReSettings reSettings = null) 38 | 39 | - RulesEngine(ILogger logger = null, ReSettings reSettings = null) 40 | + RulesEngine(ReSettings reSettings = null) 41 | ``` 42 | - Obsolete methods and properties have been removed, from the follow models:- 43 | - RuleResultTree 44 | - `ToResultTreeMessages()` has been removed from `RuleResultTree` model 45 | - `GetMessages()` has been removed from `RuleResultTree` model 46 | - `RuleEvaluatedParams` has been removed from `RuleResultTree` model, Please use `Inputs` instead 47 | 48 | - Workflow 49 | - `WorkflowRulesToInject` has been removed, Please use `WorkflowsToInject` instead 50 | - `ErrorType` has been removed from `Rule` 51 | 52 | - Resettings 53 | - `EnableLocalParams` has been removed from `ReSettings`, Please use `EnableScopedParams` instead 54 | 55 | 56 | ## [3.5.0] 57 | - `EvaluateRule` action now support custom inputs and filtered inputs 58 | - Added `ContainsWorkflow` method in RulesEngine (by @okolobaxa) 59 | - Fixed minor bugs (#258 & #259) 60 | 61 | ## [3.4.0] 62 | - Made RulesEngine Strong Name and Authenticode signed 63 | - Renamed few models to streamline names (by @alexrich) 64 | - `WorkflowRules` is renamed to `Workflow` 65 | - `WorkflowRulesToInject` is renamed to `WorkflowsToInject` 66 | - `RuleAction` is renamed to `RuleActions` 67 | 68 | **Note**: The old models are still supported but will be removed with version 4.0.0 69 | 70 | 71 | ## [3.3.0] 72 | - Added support for actions in nested rules 73 | - Improved serialization support for System.Text.Json for workflow model 74 | 75 | Breaking Change: 76 | - Type of Action has been changed from `Dictionary` to `RuleActions` 77 | - No impact if you are serializing workflow from json 78 | - For workflow objects created in code, refer - [link](https://github.com/microsoft/RulesEngine/pull/182/files#diff-a5093dda2dcc1e4958ce3533edb607bb61406e1f0a9071eca4e317bdd987c0d3) 79 | 80 | ## [3.2.0] 81 | - Added AddOrUpdateWorkflow method to update workflows atomically (by @AshishPrasad) 82 | - Updated dependencies to latest 83 | 84 | Breaking Change: 85 | - `AddWorkflow` now throws exception if you try to add a workflow which already exists. 86 | Use `AddOrUpdateWorkflow` to update existing workflow 87 | 88 | ## [3.1.0] 89 | - Added globalParams feature which can be applied to all rules 90 | - Enabled localParams support for nested Rules 91 | - Made certain fields in Rule model optional allowing users to define workflow with minimal fields 92 | - Added option to disable Rule in workflow json 93 | - Added `GetAllRegisteredWorkflow` to RulesEngine to return all registered workflows 94 | - Runtime errors for expressions will now be logged as errorMessage instead of throwing Exceptions by default 95 | - Fixed RuleParameter passed as null 96 | 97 | ## [3.0.2] 98 | - Fixed LocalParams cache not getting cleaned up when RemoveWorkflows and ClearWorkflows are called 99 | 100 | ## [3.0.1] 101 | - Moved ActionResult and ActionRuleResult under RulesEngine.Models namespace 102 | 103 | 104 | ## [3.0.0] 105 | ### Major Enhancements 106 | - Added support for Actions. More details on [actions wiki](https://github.com/microsoft/RulesEngine/wiki/Actions) 107 | - Major performance improvement 108 | - 25% improvement from previous version 109 | - Upto 35% improvement by disabling optional features 110 | - RulesEngine now virtually supports unlimited inputs (Previous limitation was 16 inputs) 111 | - RuleExpressionParser is now available to use expression evaluation outside RulesEngine 112 | 113 | ### Breaking Changes 114 | - `ExecuteRule` method has been renamed to `ExecuteAllRulesAsync` 115 | - `Input` field in RuleResultTree has been changed to `Inputs` which returns all the the inputs as Dictionary of name and value pair 116 | 117 | ## [2.1.5] - 02-11-2020 118 | - Added `Properties` field to Rule to allow custom fields to Rule 119 | 120 | ## [2.1.4] - 15-10-2020 121 | - Added exception data properties to identify RuleName. 122 | 123 | ## [2.1.3] - 12-10-2020 124 | - Optional parameter for rethrow exception on failure of expression compilation. 125 | 126 | ## [2.1.2] - 02-10-2020 127 | - Fixed binary expression requirement. Now any expression will work as long as it evalutes to boolean. 128 | 129 | ## [2.1.1] - 01-09-2020 130 | - Fixed exception thrown when errormessage field is null 131 | - Added better messaging when identifier is not found in expression 132 | - Fixed other minor bugs 133 | 134 | ## [2.1.0] - 18-05-2020 135 | - Adding local param support to make expression authroing more intuitive. 136 | 137 | ## [2.0.0] - 18-05-2020 138 | ### Changed 139 | - Interface simplified by removing redundant parameters in the IRulesEngine. 140 | - Custom Logger replaced with Microsoft Logger. 141 | 142 | ## [1.0.2] - 16-01-2020 143 | ### Added 144 | - Cache system added so that rules compilation is stored and thus made more efficient. 145 | 146 | ### Fix 147 | - Concurrency issue which arose by dictionary was resolved. 148 | 149 | ## [1.0.1] - 24-09-2019 150 | ### Added 151 | - Exceptions handling scenario in the case a rule execution throws an exception 152 | 153 | ## [1.0.0] - 20-08-2019 154 | 155 | ### Added 156 | - The first version of the NuGet 157 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /RulesEngine.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31717.71 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RulesEngine", "src\RulesEngine\RulesEngine.csproj", "{CD4DFE6A-083B-478E-8377-77F474833E30}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RulesEngine.UnitTest", "test\RulesEngine.UnitTest\RulesEngine.UnitTest.csproj", "{50E0C2A5-E2C8-4B12-8C0E-B69F698A82BF}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DemoApp", "demo\DemoApp\DemoApp.csproj", "{57BB8C07-799A-4F87-A7CC-D3D3F694DD02}" 11 | ProjectSection(ProjectDependencies) = postProject 12 | {CD4DFE6A-083B-478E-8377-77F474833E30} = {CD4DFE6A-083B-478E-8377-77F474833E30} 13 | EndProjectSection 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{019DF693-8442-45B4-88C3-55CB7AFCB42E}" 16 | ProjectSection(SolutionItems) = preProject 17 | .editorconfig = .editorconfig 18 | CHANGELOG.md = CHANGELOG.md 19 | global.json = global.json 20 | README.md = README.md 21 | schema\workflow-list-schema.json = schema\workflow-list-schema.json 22 | schema\workflow-schema.json = schema\workflow-schema.json 23 | EndProjectSection 24 | EndProject 25 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RulesEngineBenchmark", "benchmark\RulesEngineBenchmark\RulesEngineBenchmark.csproj", "{C058809F-C720-4EFC-925D-A486627B238B}" 26 | ProjectSection(ProjectDependencies) = postProject 27 | {CD4DFE6A-083B-478E-8377-77F474833E30} = {CD4DFE6A-083B-478E-8377-77F474833E30} 28 | EndProjectSection 29 | EndProject 30 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DemoApp.EFDataExample", "demo\DemoApp.EFDataExample\DemoApp.EFDataExample.csproj", "{E376D3E6-6890-4C09-9EA0-3EFD9C1E036D}" 31 | EndProject 32 | Global 33 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 34 | Debug|Any CPU = Debug|Any CPU 35 | Release|Any CPU = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 38 | {CD4DFE6A-083B-478E-8377-77F474833E30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {CD4DFE6A-083B-478E-8377-77F474833E30}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {CD4DFE6A-083B-478E-8377-77F474833E30}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {CD4DFE6A-083B-478E-8377-77F474833E30}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {50E0C2A5-E2C8-4B12-8C0E-B69F698A82BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {50E0C2A5-E2C8-4B12-8C0E-B69F698A82BF}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {50E0C2A5-E2C8-4B12-8C0E-B69F698A82BF}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {50E0C2A5-E2C8-4B12-8C0E-B69F698A82BF}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {57BB8C07-799A-4F87-A7CC-D3D3F694DD02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {57BB8C07-799A-4F87-A7CC-D3D3F694DD02}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {57BB8C07-799A-4F87-A7CC-D3D3F694DD02}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {57BB8C07-799A-4F87-A7CC-D3D3F694DD02}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {C058809F-C720-4EFC-925D-A486627B238B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {C058809F-C720-4EFC-925D-A486627B238B}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {C058809F-C720-4EFC-925D-A486627B238B}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {C058809F-C720-4EFC-925D-A486627B238B}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {E376D3E6-6890-4C09-9EA0-3EFD9C1E036D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {E376D3E6-6890-4C09-9EA0-3EFD9C1E036D}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {E376D3E6-6890-4C09-9EA0-3EFD9C1E036D}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {E376D3E6-6890-4C09-9EA0-3EFD9C1E036D}.Release|Any CPU.Build.0 = Release|Any CPU 58 | EndGlobalSection 59 | GlobalSection(SolutionProperties) = preSolution 60 | HideSolutionNode = FALSE 61 | EndGlobalSection 62 | GlobalSection(ExtensibilityGlobals) = postSolution 63 | SolutionGuid = {E1F2EC8E-4005-4DFE-90ED-296D4592867A} 64 | EndGlobalSection 65 | EndGlobal 66 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | 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/). 6 | 7 | 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. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | 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). 16 | 17 | 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). 18 | 19 | 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: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | 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. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /assets/BlockDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RulesEngine/71d59dce931d45eb9b9df97e35ac6721f0083352/assets/BlockDiagram.png -------------------------------------------------------------------------------- /benchmark/RulesEngineBenchmark/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using BenchmarkDotNet.Attributes; 5 | using BenchmarkDotNet.Running; 6 | using RulesEngine.Models; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.IO; 10 | using BenchmarkDotNet.Jobs; 11 | using System.Text.Json; 12 | 13 | namespace RulesEngineBenchmark 14 | { 15 | 16 | [MemoryDiagnoser] 17 | [SimpleJob(RuntimeMoniker.Net60)] 18 | [SimpleJob(RuntimeMoniker.Net80)] 19 | [SimpleJob(RuntimeMoniker.Net90)] 20 | public class REBenchmark 21 | { 22 | private readonly RulesEngine.RulesEngine rulesEngine; 23 | private readonly object ruleInput; 24 | private readonly List workflow; 25 | 26 | private class ListItem 27 | { 28 | public int Id { get; set; } 29 | public string Value { get; set; } 30 | } 31 | 32 | 33 | public REBenchmark() 34 | { 35 | var files = Directory.GetFiles(Directory.GetCurrentDirectory(), "NestedInputDemo.json", SearchOption.AllDirectories); 36 | if (files == null || files.Length == 0) 37 | { 38 | throw new Exception("Rules not found."); 39 | } 40 | 41 | var fileData = File.ReadAllText(files[0]); 42 | workflow = JsonSerializer.Deserialize>(fileData); 43 | 44 | rulesEngine = new RulesEngine.RulesEngine(workflow.ToArray(), new ReSettings { 45 | EnableFormattedErrorMessage = false, 46 | EnableScopedParams = false 47 | }); 48 | 49 | ruleInput = new { 50 | SimpleProp = "simpleProp", 51 | NestedProp = new { 52 | SimpleProp = "nestedSimpleProp", 53 | ListProp = new List 54 | { 55 | new ListItem 56 | { 57 | Id = 1, 58 | Value = "first" 59 | }, 60 | new ListItem 61 | { 62 | Id = 2, 63 | Value = "second" 64 | } 65 | } 66 | } 67 | 68 | }; 69 | } 70 | 71 | [Params(1000, 10000)] 72 | public int N; 73 | 74 | [Benchmark] 75 | public void RuleExecutionDefault() 76 | { 77 | foreach (var workflow in workflow) 78 | { 79 | _ = rulesEngine.ExecuteAllRulesAsync(workflow.WorkflowName, ruleInput).Result; 80 | } 81 | } 82 | } 83 | public class Program 84 | { 85 | public static void Main(string[] args) 86 | { 87 | _ = BenchmarkRunner.Run(); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /benchmark/RulesEngineBenchmark/RulesEngineBenchmark.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0;net8.0;net9.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | PreserveNewest 20 | 21 | 22 | PreserveNewest 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /benchmark/RulesEngineBenchmark/Workflows/Discount.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "WorkflowName": "Discount", 4 | "Rules": [ 5 | { 6 | "RuleName": "GiveDiscount10", 7 | "SuccessEvent": "10", 8 | "ErrorMessage": "One or more adjust rules failed.", 9 | "ErrorType": "Error", 10 | "RuleExpressionType": "LambdaExpression", 11 | "Expression": "input1.country == \"india\" AND input1.loyaltyFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2" 12 | }, 13 | { 14 | "RuleName": "GiveDiscount20", 15 | "SuccessEvent": "20", 16 | "ErrorMessage": "One or more adjust rules failed.", 17 | "ErrorType": "Error", 18 | "RuleExpressionType": "LambdaExpression", 19 | "Expression": "input1.country == \"india\" AND input1.loyaltyFactor == 3 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2" 20 | }, 21 | { 22 | "RuleName": "GiveDiscount25", 23 | "SuccessEvent": "25", 24 | "ErrorMessage": "One or more adjust rules failed.", 25 | "ErrorType": "Error", 26 | "RuleExpressionType": "LambdaExpression", 27 | "Expression": "input1.country != \"india\" AND input1.loyaltyFactor >= 2 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 5" 28 | }, 29 | { 30 | "RuleName": "GiveDiscount30", 31 | "SuccessEvent": "30", 32 | "ErrorMessage": "One or more adjust rules failed.", 33 | "ErrorType": "Error", 34 | "RuleExpressionType": "LambdaExpression", 35 | "Expression": "input1.loyaltyFactor > 3 AND input1.totalPurchasesToDate >= 50000 AND input1.totalPurchasesToDate <= 100000 AND input2.totalOrders > 5 AND input3.noOfVisitsPerMonth > 15" 36 | }, 37 | { 38 | "RuleName": "GiveDiscount30NestedOrExample", 39 | "SuccessEvent": "30", 40 | "ErrorMessage": "One or more adjust rules failed.", 41 | "ErrorType": "Error", 42 | "Operator": "OrElse", 43 | "Rules":[ 44 | { 45 | "RuleName": "IsLoyalAndHasGoodSpend", 46 | "ErrorMessage": "One or more adjust rules failed.", 47 | "ErrorType": "Error", 48 | "RuleExpressionType": "LambdaExpression", 49 | "Expression": "input1.loyaltyFactor > 3 AND input1.totalPurchasesToDate >= 50000 AND input1.totalPurchasesToDate <= 100000" 50 | }, 51 | { 52 | "RuleName": "OrHasHighNumberOfTotalOrders", 53 | "ErrorMessage": "One or more adjust rules failed.", 54 | "ErrorType": "Error", 55 | "RuleExpressionType": "LambdaExpression", 56 | "Expression": "input2.totalOrders > 15" 57 | } 58 | ] 59 | }, 60 | { 61 | "RuleName": "GiveDiscount35NestedAndExample", 62 | "SuccessEvent": "35", 63 | "ErrorMessage": "One or more adjust rules failed.", 64 | "ErrorType": "Error", 65 | "Operator": "AndAlso", 66 | "Rules": [ 67 | { 68 | "RuleName": "IsLoyal", 69 | "ErrorMessage": "One or more adjust rules failed.", 70 | "ErrorType": "Error", 71 | "RuleExpressionType": "LambdaExpression", 72 | "Expression": "input1.loyaltyFactor > 3" 73 | }, 74 | { 75 | "RuleName": "AndHasTotalPurchased100000", 76 | "ErrorMessage": "One or more adjust rules failed.", 77 | "ErrorType": "Error", 78 | "RuleExpressionType": "LambdaExpression", 79 | "Expression": "input1.totalPurchasesToDate >= 100000" 80 | }, 81 | { 82 | "RuleName": "AndOtherConditions", 83 | "ErrorMessage": "One or more adjust rules failed.", 84 | "ErrorType": "Error", 85 | "RuleExpressionType": "LambdaExpression", 86 | "Expression": "input2.totalOrders > 15 AND input3.noOfVisitsPerMonth > 25" 87 | } 88 | ] 89 | } 90 | ] 91 | } 92 | ] -------------------------------------------------------------------------------- /benchmark/RulesEngineBenchmark/Workflows/NestedInputDemo.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "WorkflowName": "NestedInputDemoWorkflow1", 4 | "Rules": [ 5 | { 6 | "RuleName": "CheckNestedSimpleProp", 7 | "ErrorMessage": "One or more adjust rules failed.", 8 | "ErrorType": "Error", 9 | "RuleExpressionType": "LambdaExpression", 10 | "Expression": "input1.NestedProp.SimpleProp == \"nestedSimpleProp\"" 11 | } 12 | ] 13 | }, 14 | { 15 | "WorkflowName": "NestedInputDemoWorkflow2", 16 | "Rules": [ 17 | { 18 | "RuleName": "CheckNestedListProp", 19 | "ErrorMessage": "One or more adjust rules failed.", 20 | "ErrorType": "Error", 21 | "RuleExpressionType": "LambdaExpression", 22 | "Expression": "input1.NestedProp.ListProp[0].Id == 1 && input1.NestedProp.ListProp[1].Value == \"second\"" 23 | } 24 | ] 25 | }, 26 | 27 | { 28 | "WorkflowName": "NestedInputDemoWorkflow3", 29 | "Rules": [ 30 | { 31 | "RuleName": "CheckNestedListPropFunctions", 32 | "ErrorMessage": "One or more adjust rules failed.", 33 | "ErrorType": "Error", 34 | "RuleExpressionType": "LambdaExpression", 35 | "Expression": "input1.NestedProp.ListProp[1].Value.ToUpper() = \"SECOND\"" 36 | } 37 | ] 38 | } 39 | ] -------------------------------------------------------------------------------- /demo/DemoApp.EFDataExample/DemoApp.EFDataExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net9.0 5 | DemoApp.EFDataExample 6 | DemoApp.EFDataExample 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /demo/DemoApp.EFDataExample/RulesEngineContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.Json; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.ChangeTracking; 7 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 8 | using RulesEngine.Models; 9 | 10 | namespace RulesEngine.Data 11 | { 12 | public class RulesEngineContext : DbContext 13 | { 14 | public DbSet Workflows { get; set; } 15 | 16 | public DbSet Rules { get; set; } 17 | 18 | protected override void OnModelCreating(ModelBuilder modelBuilder) 19 | { 20 | base.OnModelCreating(modelBuilder); 21 | 22 | modelBuilder.Entity() 23 | .HasKey(k => k.Name); 24 | 25 | modelBuilder.Entity(entity => { 26 | entity.HasKey(k => k.WorkflowName); 27 | entity.Ignore(b => b.WorkflowsToInject); 28 | }); 29 | 30 | modelBuilder.Entity().HasOne().WithMany(r => r.Rules).HasForeignKey("RuleNameFK"); 31 | 32 | var serializationOptions = new JsonSerializerOptions(JsonSerializerDefaults.General); 33 | 34 | modelBuilder.Entity(entity => { 35 | entity.HasKey(k => k.RuleName); 36 | 37 | var valueComparer = new ValueComparer>( 38 | (c1, c2) => c1.SequenceEqual(c2), 39 | c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), 40 | c => c); 41 | 42 | entity.Property(b => b.Properties) 43 | .HasConversion( 44 | v => JsonSerializer.Serialize(v, serializationOptions), 45 | v => JsonSerializer.Deserialize>(v, serializationOptions)) 46 | .Metadata 47 | .SetValueComparer(valueComparer); 48 | 49 | entity.Property(p => p.Actions) 50 | .HasConversion( 51 | v => JsonSerializer.Serialize(v, serializationOptions), 52 | v => JsonSerializer.Deserialize(v, serializationOptions)); 53 | 54 | entity.Ignore(b => b.WorkflowsToInject); 55 | }); 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /demo/DemoApp.EFDataExample/RulesEngineDemoContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json; 4 | using Microsoft.EntityFrameworkCore; 5 | using RulesEngine.Data; 6 | using RulesEngine.Models; 7 | 8 | namespace DemoApp.EFDataExample 9 | { 10 | public class RulesEngineDemoContext : RulesEngineContext 11 | { 12 | public string DbPath { get; private set; } 13 | 14 | public RulesEngineDemoContext() 15 | { 16 | var folder = Environment.SpecialFolder.LocalApplicationData; 17 | var path = Environment.GetFolderPath(folder); 18 | DbPath = $"{path}{System.IO.Path.DirectorySeparatorChar}RulesEngineDemo.db"; 19 | } 20 | protected override void OnConfiguring(DbContextOptionsBuilder options) 21 | => options.UseSqlite($"Data Source={DbPath}"); 22 | 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /demo/DemoApp/BasicDemo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.Models; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Dynamic; 8 | using static RulesEngine.Extensions.ListofRuleResultTreeExtension; 9 | 10 | namespace DemoApp 11 | { 12 | public class BasicDemo 13 | { 14 | public void Run() 15 | { 16 | Console.WriteLine($"Running {nameof(BasicDemo)}...."); 17 | List workflows = new List(); 18 | Workflow workflow = new Workflow(); 19 | workflow.WorkflowName = "Test Workflow Rule 1"; 20 | 21 | List rules = new List(); 22 | 23 | Rule rule = new Rule(); 24 | rule.RuleName = "Test Rule"; 25 | rule.SuccessEvent = "Count is within tolerance."; 26 | rule.ErrorMessage = "Over expected."; 27 | rule.Expression = "count < 3"; 28 | rule.RuleExpressionType = RuleExpressionType.LambdaExpression; 29 | 30 | rules.Add(rule); 31 | 32 | workflow.Rules = rules; 33 | 34 | workflows.Add(workflow); 35 | 36 | var bre = new RulesEngine.RulesEngine(workflows.ToArray(), null); 37 | 38 | dynamic datas = new ExpandoObject(); 39 | datas.count = 1; 40 | var inputs = new dynamic[] 41 | { 42 | datas 43 | }; 44 | 45 | List resultList = bre.ExecuteAllRulesAsync("Test Workflow Rule 1", inputs).Result; 46 | 47 | bool outcome = false; 48 | 49 | //Different ways to show test results: 50 | outcome = resultList.TrueForAll(r => r.IsSuccess); 51 | 52 | resultList.OnSuccess((eventName) => { 53 | Console.WriteLine($"Result '{eventName}' is as expected."); 54 | outcome = true; 55 | }); 56 | 57 | resultList.OnFail(() => { 58 | outcome = false; 59 | }); 60 | 61 | Console.WriteLine($"Test outcome: {outcome}."); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /demo/DemoApp/DemoApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0;net9.0 6 | DemoApp.Program 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | PreserveNewest 17 | 18 | 19 | PreserveNewest 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /demo/DemoApp/EFDemo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using DemoApp.EFDataExample; 5 | using RulesEngine.Models; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Dynamic; 9 | using System.IO; 10 | using System.Linq; 11 | using static RulesEngine.Extensions.ListofRuleResultTreeExtension; 12 | using Microsoft.EntityFrameworkCore; 13 | 14 | namespace DemoApp 15 | { 16 | using System.Text.Json; 17 | 18 | public class EFDemo 19 | { 20 | public void Run() 21 | { 22 | Console.WriteLine($"Running {nameof(EFDemo)}...."); 23 | var basicInfo = "{\"name\": \"hello\",\"email\": \"abcy@xyz.com\",\"creditHistory\": \"good\",\"country\": \"canada\",\"loyaltyFactor\": 3,\"totalPurchasesToDate\": 10000}"; 24 | var orderInfo = "{\"totalOrders\": 5,\"recurringItems\": 2}"; 25 | var telemetryInfo = "{\"noOfVisitsPerMonth\": 10,\"percentageOfBuyingToVisit\": 15}"; 26 | 27 | dynamic input1 = JsonSerializer.Deserialize(basicInfo); 28 | dynamic input2 = JsonSerializer.Deserialize(orderInfo); 29 | dynamic input3 = JsonSerializer.Deserialize(telemetryInfo); 30 | 31 | var inputs = new dynamic[] 32 | { 33 | input1, 34 | input2, 35 | input3 36 | }; 37 | 38 | var files = Directory.GetFiles(Directory.GetCurrentDirectory(), "Discount.json", SearchOption.AllDirectories); 39 | if (files == null || files.Length == 0) 40 | throw new Exception("Rules not found."); 41 | 42 | var fileData = File.ReadAllText(files[0]); 43 | var workflow = JsonSerializer.Deserialize>(fileData); 44 | 45 | RulesEngineDemoContext db = new RulesEngineDemoContext(); 46 | if (db.Database.EnsureCreated()) 47 | { 48 | db.Workflows.AddRange(workflow); 49 | db.SaveChanges(); 50 | } 51 | 52 | var wfr = db.Workflows.Include(i => i.Rules).ThenInclude(i => i.Rules).ToArray(); 53 | 54 | var bre = new RulesEngine.RulesEngine(wfr, null); 55 | 56 | string discountOffered = "No discount offered."; 57 | 58 | List resultList = bre.ExecuteAllRulesAsync("Discount", inputs).Result; 59 | 60 | resultList.OnSuccess((eventName) => { 61 | discountOffered = $"Discount offered is {eventName} % over MRP."; 62 | }); 63 | 64 | resultList.OnFail(() => { 65 | discountOffered = "The user is not eligible for any discount."; 66 | }); 67 | 68 | Console.WriteLine(discountOffered); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /demo/DemoApp/JSONDemo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.Models; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Dynamic; 8 | using System.IO; 9 | using static RulesEngine.Extensions.ListofRuleResultTreeExtension; 10 | 11 | namespace DemoApp 12 | { 13 | using System.Text.Json; 14 | 15 | public class JSONDemo 16 | { 17 | public void Run() 18 | { 19 | Console.WriteLine($"Running {nameof(JSONDemo)}...."); 20 | var basicInfo = "{\"name\": \"hello\",\"email\": \"abcy@xyz.com\",\"creditHistory\": \"good\",\"country\": \"canada\",\"loyaltyFactor\": 3,\"totalPurchasesToDate\": 10000}"; 21 | var orderInfo = "{\"totalOrders\": 5,\"recurringItems\": 2}"; 22 | var telemetryInfo = "{\"noOfVisitsPerMonth\": 10,\"percentageOfBuyingToVisit\": 15}"; 23 | 24 | 25 | 26 | dynamic input1 = JsonSerializer.Deserialize(basicInfo); 27 | dynamic input2 = JsonSerializer.Deserialize(orderInfo); 28 | dynamic input3 = JsonSerializer.Deserialize(telemetryInfo); 29 | 30 | var inputs = new dynamic[] 31 | { 32 | input1, 33 | input2, 34 | input3 35 | }; 36 | 37 | var files = Directory.GetFiles(Directory.GetCurrentDirectory(), "Discount.json", SearchOption.AllDirectories); 38 | if (files == null || files.Length == 0) 39 | throw new Exception("Rules not found."); 40 | 41 | var fileData = File.ReadAllText(files[0]); 42 | var workflow = JsonSerializer.Deserialize>(fileData); 43 | 44 | var bre = new RulesEngine.RulesEngine(workflow.ToArray(), null); 45 | 46 | string discountOffered = "No discount offered."; 47 | 48 | List resultList = bre.ExecuteAllRulesAsync("Discount", inputs).Result; 49 | 50 | resultList.OnSuccess((eventName) => { 51 | discountOffered = $"Discount offered is {eventName} % over MRP."; 52 | }); 53 | 54 | resultList.OnFail(() => { 55 | discountOffered = "The user is not eligible for any discount."; 56 | }); 57 | 58 | Console.WriteLine(discountOffered); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /demo/DemoApp/NestedInputDemo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.Extensions; 5 | using RulesEngine.Models; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | 10 | namespace DemoApp 11 | { 12 | using System.Text.Json; 13 | 14 | internal class ListItem 15 | { 16 | public int Id { get; set; } 17 | public string Value { get; set; } 18 | } 19 | 20 | public class NestedInputDemo 21 | { 22 | public void Run() 23 | { 24 | Console.WriteLine($"Running {nameof(NestedInputDemo)}...."); 25 | var nestedInput = new { 26 | SimpleProp = "simpleProp", 27 | NestedProp = new { 28 | SimpleProp = "nestedSimpleProp", 29 | ListProp = new List 30 | { 31 | new ListItem 32 | { 33 | Id = 1, 34 | Value = "first" 35 | }, 36 | new ListItem 37 | { 38 | Id = 2, 39 | Value = "second" 40 | } 41 | } 42 | } 43 | 44 | }; 45 | 46 | var files = Directory.GetFiles(Directory.GetCurrentDirectory(), "NestedInputDemo.json", SearchOption.AllDirectories); 47 | if (files == null || files.Length == 0) 48 | { 49 | throw new Exception("Rules not found."); 50 | } 51 | 52 | var fileData = File.ReadAllText(files[0]); 53 | var Workflows = JsonSerializer.Deserialize>(fileData); 54 | 55 | var bre = new RulesEngine.RulesEngine(Workflows.ToArray(), null); 56 | foreach (var workflow in Workflows) 57 | { 58 | var resultList = bre.ExecuteAllRulesAsync(workflow.WorkflowName, nestedInput).Result; 59 | 60 | resultList.OnSuccess((eventName) => { 61 | Console.WriteLine($"{workflow.WorkflowName} evaluation resulted in success - {eventName}"); 62 | }).OnFail(() => { 63 | Console.WriteLine($"{workflow.WorkflowName} evaluation resulted in failure"); 64 | }); 65 | 66 | } 67 | 68 | 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /demo/DemoApp/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | namespace DemoApp 5 | { 6 | public static class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | new BasicDemo().Run(); 11 | new JSONDemo().Run(); 12 | new NestedInputDemo().Run(); 13 | new EFDemo().Run(); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /demo/DemoApp/Workflows/Discount.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "WorkflowName": "Discount", 4 | "Rules": [ 5 | { 6 | "RuleName": "GiveDiscount10", 7 | "SuccessEvent": "10", 8 | "ErrorMessage": "One or more adjust rules failed.", 9 | "ErrorType": "Error", 10 | "RuleExpressionType": "LambdaExpression", 11 | "Expression": "input1.country == \"india\" AND input1.loyaltyFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2" 12 | }, 13 | { 14 | "RuleName": "GiveDiscount20", 15 | "SuccessEvent": "20", 16 | "ErrorMessage": "One or more adjust rules failed.", 17 | "ErrorType": "Error", 18 | "RuleExpressionType": "LambdaExpression", 19 | "Expression": "input1.country == \"india\" AND input1.loyaltyFactor == 3 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2" 20 | }, 21 | { 22 | "RuleName": "GiveDiscount25", 23 | "SuccessEvent": "25", 24 | "ErrorMessage": "One or more adjust rules failed.", 25 | "ErrorType": "Error", 26 | "RuleExpressionType": "LambdaExpression", 27 | "Expression": "input1.country != \"india\" AND input1.loyaltyFactor >= 2 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 5" 28 | }, 29 | { 30 | "RuleName": "GiveDiscount30", 31 | "SuccessEvent": "30", 32 | "ErrorMessage": "One or more adjust rules failed.", 33 | "ErrorType": "Error", 34 | "RuleExpressionType": "LambdaExpression", 35 | "Expression": "input1.loyaltyFactor > 3 AND input1.totalPurchasesToDate >= 50000 AND input1.totalPurchasesToDate <= 100000 AND input2.totalOrders > 5 AND input3.noOfVisitsPerMonth > 15" 36 | }, 37 | { 38 | "RuleName": "GiveDiscount30NestedOrExample", 39 | "SuccessEvent": "30", 40 | "ErrorMessage": "One or more adjust rules failed.", 41 | "ErrorType": "Error", 42 | "Operator": "OrElse", 43 | "Rules":[ 44 | { 45 | "RuleName": "IsLoyalAndHasGoodSpend", 46 | "ErrorMessage": "One or more adjust rules failed.", 47 | "ErrorType": "Error", 48 | "RuleExpressionType": "LambdaExpression", 49 | "Expression": "input1.loyaltyFactor > 3 AND input1.totalPurchasesToDate >= 50000 AND input1.totalPurchasesToDate <= 100000" 50 | }, 51 | { 52 | "RuleName": "OrHasHighNumberOfTotalOrders", 53 | "ErrorMessage": "One or more adjust rules failed.", 54 | "ErrorType": "Error", 55 | "RuleExpressionType": "LambdaExpression", 56 | "Expression": "input2.totalOrders > 15" 57 | } 58 | ] 59 | }, 60 | { 61 | "RuleName": "GiveDiscount35NestedAndExample", 62 | "SuccessEvent": "35", 63 | "ErrorMessage": "One or more adjust rules failed.", 64 | "ErrorType": "Error", 65 | "Operator": "AndAlso", 66 | "Rules": [ 67 | { 68 | "RuleName": "IsLoyal", 69 | "ErrorMessage": "One or more adjust rules failed.", 70 | "ErrorType": "Error", 71 | "RuleExpressionType": "LambdaExpression", 72 | "Expression": "input1.loyaltyFactor > 3" 73 | }, 74 | { 75 | "RuleName": "AndHasTotalPurchased100000", 76 | "ErrorMessage": "One or more adjust rules failed.", 77 | "ErrorType": "Error", 78 | "RuleExpressionType": "LambdaExpression", 79 | "Expression": "input1.totalPurchasesToDate >= 100000" 80 | }, 81 | { 82 | "RuleName": "AndOtherConditions", 83 | "ErrorMessage": "One or more adjust rules failed.", 84 | "ErrorType": "Error", 85 | "RuleExpressionType": "LambdaExpression", 86 | "Expression": "input2.totalOrders > 15 AND input3.noOfVisitsPerMonth > 25" 87 | } 88 | ] 89 | } 90 | ] 91 | } 92 | ] -------------------------------------------------------------------------------- /demo/DemoApp/Workflows/NestedInputDemo.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "WorkflowName": "NestedInputDemoWorkflow1", 4 | "Rules": [ 5 | { 6 | "RuleName": "CheckNestedSimpleProp", 7 | "ErrorMessage": "One or more adjust rules failed.", 8 | "ErrorType": "Error", 9 | "RuleExpressionType": "LambdaExpression", 10 | "Expression": "input1.NestedProp.SimpleProp == \"nestedSimpleProp\"" 11 | } 12 | ] 13 | }, 14 | { 15 | "WorkflowName": "NestedInputDemoWorkflow2", 16 | "Rules": [ 17 | { 18 | "RuleName": "CheckNestedListProp", 19 | "ErrorMessage": "One or more adjust rules failed.", 20 | "ErrorType": "Error", 21 | "RuleExpressionType": "LambdaExpression", 22 | "Expression": "input1.NestedProp.ListProp[0].Id == 1 && input1.NestedProp.ListProp[1].Value == \"second\"" 23 | } 24 | ] 25 | }, 26 | 27 | { 28 | "WorkflowName": "NestedInputDemoWorkflow3", 29 | "Rules": [ 30 | { 31 | "RuleName": "CheckNestedListPropFunctions", 32 | "ErrorMessage": "One or more adjust rules failed.", 33 | "ErrorType": "Error", 34 | "RuleExpressionType": "LambdaExpression", 35 | "Expression": "input1.NestedProp.ListProp[1].Value.ToUpper() = \"SECOND\"" 36 | } 37 | ] 38 | } 39 | ] -------------------------------------------------------------------------------- /deployment/build-signed.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(Mandatory)] 3 | [string] $csprojFilePath, 4 | [Parameter(Mandatory)] 5 | [string] $signingKey 6 | ) 7 | 8 | # sign and build the project 9 | $directory = Split-Path $csprojFilePath; 10 | $signKeyFile = Join-Path $directory "signKey.snk"; 11 | 12 | $bytes = [Convert]::FromBase64String($signingKey) 13 | [IO.File]::WriteAllBytes($signKeyFile, $bytes) 14 | 15 | dotnet build $csprojFilePath -c Release -p:ContinuousIntegrationBuild=true -p:DelaySign=false -p:AssemblyOriginatorKeyFile=$signKeyFile -------------------------------------------------------------------------------- /docs/Home.md: -------------------------------------------------------------------------------- 1 | ## Welcome 2 | Welcome to the RulesEngine Wiki! 3 | 4 | The pages here are primarily intended for those who wish to contribute to the Rules Engine Project by suggesting new features or building extensions or submitting pull requests. 5 | 6 | This Wiki also includes a demo along with the explanation of different features of the project so that the using of the application can be easily understood. 7 | 8 | ## About 9 | Deep dive into the project code and the Wiki to find different features and workings of the project. 10 | 11 | Search for the solution or file a new issue in [GitHub](https://github.com/microsoft/RulesEngine/issues) if you find something broken in the code. 12 | 13 | ## Contributing 14 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 15 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 16 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 17 | 18 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 19 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 20 | provided by the bot. You will only need to do this once across all repos using our CLA. 21 | 22 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 23 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 24 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 25 | -------------------------------------------------------------------------------- /docs/Introduction.md: -------------------------------------------------------------------------------- 1 | ## What is the Rules Engine 2 | While building any application, the crux or the core part of it is always business logic or business rules. And as with any application, there always comes a time when some or a lot of the rules or policies change in the system. But with that change, comes a lot of rework like changing design or creating a new module altogether to code in the changes in the rules, regression testing, performance testing etc. The rework along with debugging if required amounts to a lot of unnecessary work which can otherwise be utilized for other work, thus reducing the engineering cycle by drastic amounts. 3 | 4 | In this library, we have abstracted the rules so that the core logic is always maintained while the rules change can happen in an easy way without changing the code base. Also, the input to the system is dynamic in nature so the model need not be defined in the system. It can be sent as an expando object or any other typed object and the system will be able to handle it. 5 | 6 | These all features make this library highly configurable and extensible as shown in [Getting Started with Rules Engine](https://github.com/microsoft/RulesEngine/wiki/Getting-Started). 7 | 8 | 9 | ### How it works 10 | 11 | ![](https://github.com/microsoft/RulesEngine/blob/main/assets/BlockDiagram.png) 12 | 13 | Here. there are multiple actors/component involved. 14 | ##### Rules Engine 15 | This component is the Rules Engine library/NuGet package being referenced by the developer. 16 | ##### Rules Store 17 | As shown in [Rules Schema](https://github.com/microsoft/RulesEngine/wiki/Getting-Started#rules-schema), we need rules in a particular format for the library to work. While the rules structure is rigid, the data itself can be stored in any component and can be accessed in any form the developer chooses. It can be stored in the form of json files in file structure or blob, or as documents in cosmos db, or any database, azure app configuration or any other place the developer thinks is going to be appropriate based on the project requirements. 18 | ##### Input 19 | The input(s) for the system can be taken from any place as well like user input, blobs, databases, service bus or any other system. 20 | ##### Wrapper 21 | This library sits as a black box outside the project as a referenced project or NuGet package. Then the user can create a wrapper around the library, which will get the rules from the rules store and convert it into the WorkFlowRules structure and send it to the RulesEngine along with the input(s). The RulesEngine then computes and give the information to the wrapper and the wrapper can then do whatever the logic demands with the output information. 22 | 23 | -------------------------------------------------------------------------------- /docs/Use-Case.md: -------------------------------------------------------------------------------- 1 | ## Use Case 2 | The use case for demo purposes used here is explained as follows. The system we are designing is an e-commerce discount calculation system. 3 | 4 | ### Rules 5 | The rules for the discount calculation are – 6 | 7 | 1. Give the user a discount of 10% over MRP if the following conditions are followed – 8 | * The user’s country is India. 9 | * The user’s loyalty factor is less than or equal to 2. 10 | * All the orders purchased by the user so far should amount to more than 5,000. 11 | * User should have at least made more than two successful orders. 12 | * The user should have visited the site more than two times every month. 13 | 2. Give the user a discount of 20% over MRP if the following conditions are followed – 14 | * The user’s country is India. 15 | * The user’s loyalty factor is equal to 3. 16 | * All the orders purchased by the user so far should amount to more than 10,000. 17 | * User should have at least made more than two successful orders. 18 | * The user should have visited the site more than two times every month. 19 | 3. Give the user a discount of 25% over MRP if the following conditions are followed – 20 | * The user’s country is not India. 21 | * The user’s loyalty factor is greater than or equal to 2. 22 | * All the orders purchased by the user so far should amount to more than 10,000. 23 | * User should have at least made more than two successful orders. 24 | * The user should have visited the site more than five times every month. 25 | 4. Give the user a discount of 30% over MRP if the following conditions are followed – 26 | * The user’s loyalty factor is greater than 3. 27 | * All the orders purchased by the user so far should amount to more than 50,000 but less than 100,000. 28 | * User should have at least made more than five successful orders. 29 | * The user should have visited the site more than fifteen times every month. 30 | 5. Give the user a discount of 30% over MRP if the following conditions are followed – 31 | * The user’s loyalty factor is greater than 3. 32 | * All the orders purchased by the user so far should amount to more than 100,000. 33 | * User should have at least made more than fifteen successful orders. 34 | * The user should have visited the site more than 25 times every month. 35 | 6. Give 0% discount in any other case. 36 | 37 | ### Inputs 38 | Here the inputs will be of three different types as they are coming from three different data sources/APIs. 39 | #### User Basic Info 40 | This input has information like – 41 | * Name 42 | * Country 43 | * Email 44 | * Credit history 45 | * Loyalty factor 46 | * Sum of the purchases made by the user till date. 47 | 48 | #### Users Order Information 49 | This input is a summarization of the orders made by the user so far. This input has information like – 50 | * Total number of orders 51 | * Recurring items in those orders if any 52 | 53 | #### Users Telemetry Information 54 | This input is a summarization of the telemetry information collected based on the user’s visit to the site. This input has information like – 55 | * Number of visits to the site per month 56 | * Percentage of the number of times the user purchased something to the number of times the user visited 57 | -------------------------------------------------------------------------------- /docs/_Sidebar.md: -------------------------------------------------------------------------------- 1 | [Home](https://github.com/microsoft/RulesEngine/wiki) 2 | * [Welcome](https://github.com/microsoft/RulesEngine/wiki#welcome) 3 | * [About](https://github.com/microsoft/RulesEngine/wiki#about) 4 | * [Contributing](https://github.com/microsoft/RulesEngine/wiki#contributing) 5 | 6 | [Introduction](https://github.com/microsoft/RulesEngine/wiki/Introduction) 7 | * [What is the Rules Engine](https://github.com/microsoft/RulesEngine/wiki/Introduction#what-is-the-rules-engine) 8 | * [How it works](https://github.com/microsoft/RulesEngine/wiki/Introduction#how-it-works) 9 | * [Rules Engine](https://github.com/microsoft/RulesEngine/wiki/Introduction#rules-engine) 10 | * [Rules Store](https://github.com/microsoft/RulesEngine/wiki/Introduction#rules-store) 11 | * [Input](https://github.com/microsoft/RulesEngine/wiki/Introduction#input) 12 | * [Wrapper](https://github.com/microsoft/RulesEngine/wiki/Introduction#wrapper) 13 | 14 | [Getting Started](https://github.com/microsoft/RulesEngine/wiki/Getting-Started) 15 | * [Getting Started with Rules Engine](https://github.com/microsoft/RulesEngine/wiki/Getting-Started#getting-started-with-rules-engine) 16 | * [Publicly accessible interfaces, models, methods and schemas](https://github.com/microsoft/RulesEngine/wiki/Getting-Started#publicly-accessible-interfaces-models-methods-and-schemas) 17 | * [Rules](https://github.com/microsoft/RulesEngine/wiki/Getting-Started#rules) 18 | * [Rules Schema](https://github.com/microsoft/RulesEngine/wiki/Getting-Started#rules-schema) 19 | * [Logger](https://github.com/microsoft/RulesEngine/wiki/Getting-Started#logger) 20 | * [ReSettings](https://github.com/microsoft/RulesEngine/wiki/Getting-Started#resettings) 21 | * [LocalParams](https://github.com/microsoft/RulesEngine/wiki/Getting-Started#localparams) 22 | * [RuleParameter](https://github.com/microsoft/RulesEngine/wiki/Getting-Started#ruleparameter) 23 | * [RuleResultTree](https://github.com/microsoft/RulesEngine/wiki/Getting-Started#ruleresulttree) 24 | * [IRulesEngine](https://github.com/microsoft/RulesEngine/wiki/Getting-Started#irulesengine) 25 | * [Initiating the Rules Engine](https://github.com/microsoft/RulesEngine/wiki/Getting-Started#initiating-the-rules-engine) 26 | * [Success/Failure](https://github.com/microsoft/RulesEngine/wiki/Getting-Started#successfailure) 27 | * [How to use Rules Engine](https://github.com/microsoft/RulesEngine/wiki/Getting-Started#how-to-use-rules-engine) 28 | 29 | [Use Case](https://github.com/microsoft/RulesEngine/wiki/Use-Case) 30 | * [Use Case](https://github.com/microsoft/RulesEngine/wiki/Use-Case#use-case) 31 | * [Rules](https://github.com/microsoft/RulesEngine/wiki/Use-Case#rules) 32 | * [Inputs](https://github.com/microsoft/RulesEngine/wiki/Use-Case#inputs) 33 | * [User Basic Info](https://github.com/microsoft/RulesEngine/wiki/Use-Case#user-basic-info) 34 | * [Users Order Information](https://github.com/microsoft/RulesEngine/wiki/Use-Case#users-order-information) 35 | * [Users Telemetry Information](https://github.com/microsoft/RulesEngine/wiki/Use-Case#users-telemetry-information) -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.0", 4 | "rollForward": "latestFeature", 5 | "allowPrerelease": false 6 | } 7 | } -------------------------------------------------------------------------------- /schema/workflow-list-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "type": "array", 4 | "items": { 5 | "$ref": "https://raw.githubusercontent.com/microsoft/RulesEngine/main/schema/workflow-schema.json" 6 | } 7 | } -------------------------------------------------------------------------------- /schema/workflow-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "definitions": { 4 | "ScopedParam": { 5 | "type": "object", 6 | "properties": { 7 | "Name": { "type": "string" }, 8 | "Expression": { "type": "string" } 9 | }, 10 | "required": [ "Name", "Expression" ] 11 | }, 12 | "Rule": { 13 | "title": "Rule", 14 | "properties": { 15 | "RuleName": { 16 | "type": "string" 17 | }, 18 | "LocalParams": { 19 | "type": "array", 20 | "items": { "$ref": "#/definitions/ScopedParam" } 21 | }, 22 | "Operator": { 23 | "enum": [ 24 | "And", 25 | "AndAlso", 26 | "Or", 27 | "OrElse" 28 | ] 29 | }, 30 | "ErrorMessage": { 31 | "type": "string" 32 | }, 33 | "SuccessEvent": { 34 | "type": "string" 35 | }, 36 | "Rules": { 37 | "type": "array", 38 | "items": { 39 | "anyOf": [ 40 | { 41 | "$ref": "#/definitions/LeafRule" 42 | }, 43 | { 44 | "$ref": "#/definitions/Rule" 45 | } 46 | ] 47 | } 48 | }, 49 | "Properties": { 50 | "type": "object" 51 | }, 52 | "Actions": { 53 | "$ref": "#/definitions/RuleActions" 54 | }, 55 | "Enabled": { 56 | "type": "boolean", 57 | "default": true 58 | } 59 | }, 60 | "required": [ 61 | "RuleName", 62 | "Operator", 63 | "Rules" 64 | ], 65 | "type": "object" 66 | }, 67 | "LeafRule": { 68 | "title": "Leaf Rule", 69 | "type": "object", 70 | "required": [ 71 | "RuleName", 72 | "Expression" 73 | ], 74 | "properties": { 75 | "RuleName": { 76 | "type": "string" 77 | }, 78 | "LocalParams": { 79 | "type": "array", 80 | "items": { "$ref": "#/definitions/ScopedParam" } 81 | }, 82 | "Expression": { 83 | "type": "string" 84 | }, 85 | "RuleExpressionType": { 86 | "enum": [ 87 | "LambdaExpression" 88 | ] 89 | }, 90 | "ErrorMessage": { 91 | "type": "string" 92 | }, 93 | "SuccessEvent": { 94 | "type": "string" 95 | }, 96 | "Properties": { 97 | "type": "object" 98 | }, 99 | "Actions": { 100 | "$ref": "#/definitions/RuleActions" 101 | }, 102 | "Enabled": { 103 | "type": "boolean", 104 | "default": true 105 | } 106 | } 107 | }, 108 | "ActionInfo": { 109 | "properties": { 110 | "Name": { 111 | "type": "string" 112 | }, 113 | "Context": { 114 | "type": "object" 115 | } 116 | }, 117 | "required": [ 118 | "Name" 119 | ] 120 | }, 121 | "RuleActions": { 122 | "properties": { 123 | "OnSuccess": { 124 | "$ref": "#/definitions/ActionInfo" 125 | }, 126 | "OnFailure": { 127 | "$ref": "#/definitions/ActionInfo" 128 | } 129 | } 130 | } 131 | 132 | }, 133 | "properties": { 134 | "WorkflowName": { 135 | "type": "string" 136 | }, 137 | "WorkflowsToInject": { 138 | "type": "array", 139 | "items": { "type": "string" } 140 | }, 141 | "GlobalParams": { 142 | "type": "array", 143 | "items": { "$ref": "#/definitions/ScopedParam" } 144 | }, 145 | "Rules": { 146 | "type": "array", 147 | "items": { 148 | "anyOf": [ 149 | { 150 | "$ref": "#/definitions/LeafRule" 151 | }, 152 | { 153 | "$ref": "#/definitions/Rule" 154 | } 155 | ] 156 | } 157 | } 158 | }, 159 | "required": [ 160 | "WorkflowName", 161 | "Rules" 162 | ], 163 | "type": "object" 164 | } 165 | -------------------------------------------------------------------------------- /scripts/check-coverage.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(Mandatory=$true)][string] $reportPath, 3 | [Parameter(Mandatory=$true)][decimal] $threshold 4 | ) 5 | 6 | 7 | [XML]$report = Get-Content $reportPath; 8 | [decimal]$coverage = [decimal]$report.coverage.'line-rate' * 100; 9 | 10 | if ($coverage -lt $threshold) { 11 | Write-Error "Coverage($coverage) is less than $threshold percent" 12 | exit 1 13 | } 14 | else{ 15 | Write-Host "Coverage($coverage) is more than $threshold percent" 16 | } 17 | -------------------------------------------------------------------------------- /scripts/generate-coverage-report.ps1: -------------------------------------------------------------------------------- 1 | dotnet tool restore 2 | dotnet reportgenerator "-reports:**/coverage.cobertura.xml" "-targetdir:coveragereport" -reporttypes:"Html;lcov;Cobertura" -------------------------------------------------------------------------------- /signing/RulesEngine-publicKey.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RulesEngine/71d59dce931d45eb9b9df97e35ac6721f0083352/signing/RulesEngine-publicKey.snk -------------------------------------------------------------------------------- /src/RulesEngine/Actions/ActionBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.Models; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading.Tasks; 8 | 9 | namespace RulesEngine.Actions 10 | { 11 | public abstract class ActionBase 12 | { 13 | internal async virtual ValueTask ExecuteAndReturnResultAsync(ActionContext context, RuleParameter[] ruleParameters, bool includeRuleResults = false) 14 | { 15 | var result = new ActionRuleResult(); 16 | try 17 | { 18 | result.Output = await Run(context, ruleParameters); 19 | } 20 | catch (Exception ex) 21 | { 22 | result.Exception = new Exception($"Exception while executing {GetType().Name}: {ex.Message}", ex); 23 | } 24 | finally 25 | { 26 | if (includeRuleResults) 27 | { 28 | result.Results = new List() 29 | { 30 | context.GetParentRuleResult() 31 | }; 32 | } 33 | } 34 | return result; 35 | } 36 | public abstract ValueTask Run(ActionContext context, RuleParameter[] ruleParameters); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/RulesEngine/Actions/ActionContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.Models; 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | namespace RulesEngine.Actions 9 | { 10 | using System.Text.Json; 11 | 12 | public class ActionContext 13 | { 14 | private readonly IDictionary _context; 15 | private readonly RuleResultTree _parentResult; 16 | 17 | public ActionContext(IDictionary context, RuleResultTree parentResult) 18 | { 19 | _context = new Dictionary(StringComparer.OrdinalIgnoreCase); 20 | foreach (var kv in context) 21 | { 22 | string key = kv.Key; 23 | string value; 24 | switch (kv.Value.GetType().Name) 25 | { 26 | case "String": 27 | case "JsonElement": 28 | value = kv.Value.ToString(); 29 | break; 30 | default: 31 | value = JsonSerializer.Serialize(kv.Value); 32 | break; 33 | 34 | } 35 | _context.Add(key, value); 36 | } 37 | _parentResult = parentResult; 38 | } 39 | 40 | public RuleResultTree GetParentRuleResult() 41 | { 42 | return _parentResult; 43 | } 44 | 45 | public bool TryGetContext(string name,out T output) 46 | { 47 | try 48 | { 49 | //key not found return 50 | //Returning a KeyNotFoundException has a significant impact on performance. 51 | if (!_context.ContainsKey(name)) 52 | { 53 | output = default(T); 54 | return false; 55 | } 56 | output = GetContext(name); 57 | return true; 58 | } 59 | catch(ArgumentException) 60 | { 61 | output = default(T); 62 | return false; 63 | } 64 | } 65 | 66 | public T GetContext(string name) 67 | { 68 | try 69 | { 70 | if (typeof(T) == typeof(string)) 71 | { 72 | return (T)Convert.ChangeType(_context[name], typeof(T)); 73 | } 74 | return JsonSerializer.Deserialize(_context[name]); 75 | } 76 | catch (KeyNotFoundException) 77 | { 78 | throw new ArgumentException($"Argument `{name}` was not found in the action context"); 79 | } 80 | catch (JsonException) 81 | { 82 | throw new ArgumentException($"Failed to convert argument `{name}` to type `{typeof(T).Name}` in the action context"); 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/RulesEngine/Actions/ActionFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace RulesEngine.Actions 8 | { 9 | internal class ActionFactory 10 | { 11 | private readonly IDictionary> _actionRegistry; 12 | 13 | internal ActionFactory() 14 | { 15 | _actionRegistry = new Dictionary>(StringComparer.OrdinalIgnoreCase); 16 | } 17 | internal ActionFactory(IDictionary> actionRegistry) : this() 18 | { 19 | foreach (var kv in actionRegistry) 20 | { 21 | _actionRegistry.Add(kv.Key, kv.Value); 22 | } 23 | } 24 | 25 | internal ActionBase Get(string name) 26 | { 27 | if (_actionRegistry.ContainsKey(name)) 28 | { 29 | return _actionRegistry[name](); 30 | } 31 | throw new KeyNotFoundException($"Action with name: {name} does not exist"); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/RulesEngine/Actions/EvaluateRuleAction.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.ExpressionBuilders; 5 | using RulesEngine.Models; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | 11 | namespace RulesEngine.Actions 12 | { 13 | public class EvaluateRuleAction : ActionBase 14 | { 15 | private readonly RulesEngine _ruleEngine; 16 | private readonly RuleExpressionParser _ruleExpressionParser; 17 | 18 | public EvaluateRuleAction(RulesEngine ruleEngine, RuleExpressionParser ruleExpressionParser) 19 | { 20 | _ruleEngine = ruleEngine; 21 | _ruleExpressionParser = ruleExpressionParser; 22 | } 23 | 24 | internal async override ValueTask ExecuteAndReturnResultAsync(ActionContext context, RuleParameter[] ruleParameters, bool includeRuleResults = false) 25 | { 26 | var innerResult = await base.ExecuteAndReturnResultAsync(context, ruleParameters, includeRuleResults); 27 | var output = innerResult.Output as ActionRuleResult; 28 | List resultList = null; 29 | if (includeRuleResults) 30 | { 31 | resultList = new List(output?.Results ?? new List() { }); 32 | resultList.AddRange(innerResult.Results); 33 | } 34 | return new ActionRuleResult { 35 | Output = output?.Output, 36 | Exception = innerResult.Exception, 37 | Results = resultList 38 | }; 39 | } 40 | 41 | public async override ValueTask Run(ActionContext context, RuleParameter[] ruleParameters) 42 | { 43 | var workflowName = context.GetContext("workflowName"); 44 | var ruleName = context.GetContext("ruleName"); 45 | var filteredRuleParameters = new List(ruleParameters); 46 | if(context.TryGetContext>("inputFilter",out var inputFilter)) 47 | { 48 | filteredRuleParameters = ruleParameters.Where(c => inputFilter.Contains(c.Name)).ToList(); 49 | } 50 | if (context.TryGetContext>("additionalInputs", out var additionalInputs)) 51 | { 52 | foreach(var additionalInput in additionalInputs) 53 | { 54 | dynamic value = _ruleExpressionParser.Evaluate(additionalInput.Expression, ruleParameters); 55 | filteredRuleParameters.Add(new RuleParameter(additionalInput.Name, value)); 56 | 57 | } 58 | } 59 | 60 | var ruleResult = await _ruleEngine.ExecuteActionWorkflowAsync(workflowName, ruleName, filteredRuleParameters.ToArray()); 61 | return ruleResult; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/RulesEngine/Actions/ExpressionOutputAction.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.ExpressionBuilders; 5 | using RulesEngine.Models; 6 | using System.Threading.Tasks; 7 | 8 | namespace RulesEngine.Actions 9 | { 10 | public class OutputExpressionAction : ActionBase 11 | { 12 | private readonly RuleExpressionParser _ruleExpressionParser; 13 | 14 | public OutputExpressionAction(RuleExpressionParser ruleExpressionParser) 15 | { 16 | _ruleExpressionParser = ruleExpressionParser; 17 | } 18 | 19 | public override ValueTask Run(ActionContext context, RuleParameter[] ruleParameters) 20 | { 21 | var expression = context.GetContext("expression"); 22 | return new ValueTask(_ruleExpressionParser.Evaluate(expression, ruleParameters)); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/RulesEngine/CustomTypeProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.HelperFunctions; 5 | using System; 6 | using System.Collections; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Linq.Dynamic.Core; 10 | using System.Linq.Dynamic.Core.CustomTypeProviders; 11 | 12 | namespace RulesEngine 13 | { 14 | public class CustomTypeProvider : DefaultDynamicLinqCustomTypeProvider 15 | { 16 | private readonly HashSet _types; 17 | 18 | public CustomTypeProvider(Type[] types) : base(ParsingConfig.Default) 19 | { 20 | _types = new HashSet(types ?? Array.Empty()); 21 | 22 | _types.Add(typeof(ExpressionUtils)); 23 | 24 | _types.Add(typeof(Enumerable)); 25 | 26 | var queue = new Queue(_types); 27 | while (queue.Count > 0) 28 | { 29 | var t = queue.Dequeue(); 30 | 31 | var baseType = t.BaseType; 32 | if (baseType != null && _types.Add(baseType)) 33 | queue.Enqueue(baseType); 34 | 35 | foreach (var interfaceType in t.GetInterfaces()) 36 | { 37 | if (_types.Add(interfaceType)) 38 | queue.Enqueue(interfaceType); 39 | } 40 | } 41 | 42 | _types.Add(typeof(IEnumerable)); 43 | } 44 | 45 | public override HashSet GetCustomTypes() 46 | { 47 | var all = new HashSet(base.GetCustomTypes()); 48 | all.UnionWith(_types); 49 | return all; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/RulesEngine/Exceptions/ExpressionParserException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | 8 | namespace RulesEngine.Exceptions 9 | { 10 | public class ExpressionParserException: Exception 11 | { 12 | public ExpressionParserException(string message, string expression) : base(message) 13 | { 14 | Data.Add("Expression", expression); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/RulesEngine/Exceptions/RuleException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | 8 | namespace RulesEngine.Exceptions 9 | { 10 | public class RuleException : Exception 11 | { 12 | public RuleException(string message) : base(message) 13 | { 14 | } 15 | 16 | public RuleException(string message, Exception innerException) : base(message, innerException) 17 | { 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/RulesEngine/Exceptions/RuleValidationException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using FluentValidation; 5 | using FluentValidation.Results; 6 | using System.Collections.Generic; 7 | 8 | namespace RulesEngine.Exceptions 9 | { 10 | public class RuleValidationException : ValidationException 11 | { 12 | public RuleValidationException(string message, IEnumerable errors) : base(message, errors) 13 | { 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/RulesEngine/Exceptions/ScopedParamException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | 8 | namespace RulesEngine.Exceptions 9 | { 10 | public class ScopedParamException: Exception 11 | { 12 | public ScopedParamException(string message, Exception innerException, string scopedParamName): base(message,innerException) 13 | { 14 | Data.Add("ScopedParamName", scopedParamName); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.Exceptions; 5 | using RulesEngine.HelperFunctions; 6 | using RulesEngine.Models; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Linq.Dynamic.Core.Exceptions; 11 | using System.Linq.Expressions; 12 | 13 | namespace RulesEngine.ExpressionBuilders 14 | { 15 | internal sealed class LambdaExpressionBuilder : RuleExpressionBuilderBase 16 | { 17 | private readonly ReSettings _reSettings; 18 | private readonly RuleExpressionParser _ruleExpressionParser; 19 | 20 | internal LambdaExpressionBuilder(ReSettings reSettings, RuleExpressionParser ruleExpressionParser) 21 | { 22 | _reSettings = reSettings; 23 | _ruleExpressionParser = ruleExpressionParser; 24 | } 25 | 26 | internal override RuleFunc BuildDelegateForRule(Rule rule, RuleParameter[] ruleParams) 27 | { 28 | try 29 | { 30 | var ruleDelegate = _ruleExpressionParser.Compile(rule.Expression, ruleParams); 31 | return Helpers.ToResultTree(_reSettings, rule, null, ruleDelegate); 32 | } 33 | catch (Exception ex) 34 | { 35 | Helpers.HandleRuleException(ex,rule,_reSettings); 36 | 37 | var exceptionMessage = Helpers.GetExceptionMessage($"Exception while parsing expression `{rule?.Expression}` - {ex.Message}", 38 | _reSettings); 39 | 40 | bool func(object[] param) => false; 41 | 42 | return Helpers.ToResultTree(_reSettings, rule, null,func, exceptionMessage); 43 | } 44 | } 45 | 46 | internal override Expression Parse(string expression, ParameterExpression[] parameters, Type returnType) 47 | { 48 | try 49 | { 50 | return _ruleExpressionParser.Parse(expression, parameters, returnType); 51 | } 52 | catch(ParseException ex) 53 | { 54 | throw new ExpressionParserException(ex.Message, expression); 55 | } 56 | 57 | } 58 | 59 | internal override Func> CompileScopedParams(RuleParameter[] ruleParameters, RuleExpressionParameter[] scopedParameters) 60 | { 61 | return _ruleExpressionParser.CompileRuleExpressionParameters(ruleParameters, scopedParameters); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.Models; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq.Expressions; 8 | 9 | namespace RulesEngine.ExpressionBuilders 10 | { 11 | /// 12 | /// Base class for expression builders 13 | /// 14 | internal abstract class RuleExpressionBuilderBase 15 | { 16 | /// 17 | /// Builds the expression for rule. 18 | /// 19 | /// The rule. 20 | /// The type parameter expressions. 21 | /// The rule input exp. 22 | /// Expression type 23 | internal abstract RuleFunc BuildDelegateForRule(Rule rule, RuleParameter[] ruleParams); 24 | 25 | internal abstract Expression Parse(string expression, ParameterExpression[] parameters, Type returnType); 26 | 27 | internal abstract Func> CompileScopedParams(RuleParameter[] ruleParameters, RuleExpressionParameter[] scopedParameters); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/RulesEngine/Extensions/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace RulesEngine.Extensions 11 | { 12 | internal static class EnumerableExtensions 13 | { 14 | public static IEnumerable Safe(this IEnumerable enumerable) 15 | { 16 | return enumerable ?? Enumerable.Empty(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/RulesEngine/Extensions/ListofRuleResultTreeExtension.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.Models; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | 9 | namespace RulesEngine.Extensions 10 | { 11 | public static class ListofRuleResultTreeExtension 12 | { 13 | public delegate void OnSuccessFunc(string eventName); 14 | public delegate void OnFailureFunc(); 15 | 16 | 17 | /// 18 | /// Calls the Success Func for the first rule which succeeded among the ruleResults 19 | /// 20 | /// 21 | /// 22 | /// 23 | public static List OnSuccess(this List ruleResultTrees, OnSuccessFunc onSuccessFunc) 24 | { 25 | var successfulRuleResult = ruleResultTrees.FirstOrDefault(ruleResult => ruleResult.IsSuccess == true); 26 | if (successfulRuleResult != null) 27 | { 28 | var eventName = successfulRuleResult.Rule.SuccessEvent ?? successfulRuleResult.Rule.RuleName; 29 | onSuccessFunc(eventName); 30 | } 31 | 32 | return ruleResultTrees; 33 | } 34 | 35 | /// 36 | /// Calls the Failure Func if all rules failed in the ruleReults 37 | /// 38 | /// 39 | /// 40 | /// 41 | public static List OnFail(this List ruleResultTrees, OnFailureFunc onFailureFunc) 42 | { 43 | bool allFailure = ruleResultTrees.All(ruleResult => ruleResult.IsSuccess == false); 44 | if (allFailure) 45 | onFailureFunc(); 46 | return ruleResultTrees; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/RulesEngine/HelperFunctions/Constants.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | namespace RulesEngine.HelperFunctions 5 | { 6 | /// 7 | /// Constants 8 | /// 9 | public static class Constants 10 | { 11 | public const string WORKFLOW_NAME_NULL_ERRMSG = "Workflow name can not be null or empty"; 12 | public const string INJECT_WORKFLOW_RULES_ERRMSG = "Atleast one of Rules or WorkflowsToInject must be not empty"; 13 | public const string RULE_CATEGORY_CONFIGURED_ERRMSG = "Rule Category should be configured"; 14 | public const string RULE_NULL_ERRMSG = "Rules can not be null or zero"; 15 | public const string NESTED_RULE_NULL_ERRMSG = "Nested rules can not be null"; 16 | public const string NESTED_RULE_CONFIGURED_ERRMSG = "Nested rules can not be configured"; 17 | public const string OPERATOR_NULL_ERRMSG = "Operator can not be null"; 18 | public const string OPERATOR_INCORRECT_ERRMSG = "Operator {PropertyValue} is not allowed"; 19 | public const string RULE_NAME_NULL_ERRMSG = "Rule Name can not be null"; 20 | public const string OPERATOR_RULES_ERRMSG = "Cannot use Rules field when Operator is null"; 21 | public const string LAMBDA_EXPRESSION_EXPRESSION_NULL_ERRMSG = "Expression cannot be null or empty when RuleExpressionType is LambdaExpression"; 22 | public const string LAMBDA_EXPRESSION_OPERATOR_ERRMSG = "Cannot use Operator field when RuleExpressionType is LambdaExpression"; 23 | public const string LAMBDA_EXPRESSION_RULES_ERRMSG = "Cannot use Rules field when RuleExpressionType is LambdaExpression"; 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/RulesEngine/HelperFunctions/ExpressionUtils.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Linq; 6 | 7 | namespace RulesEngine.HelperFunctions 8 | { 9 | public static class ExpressionUtils 10 | { 11 | public static bool CheckContains(string check, string valList) 12 | { 13 | if (string.IsNullOrEmpty(check) || string.IsNullOrEmpty(valList)) 14 | return false; 15 | 16 | var list = valList.Split(',').ToList(); 17 | return list.Contains(check); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/RulesEngine/HelperFunctions/Helpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.Exceptions; 5 | using RulesEngine.Models; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Reflection; 10 | 11 | namespace RulesEngine.HelperFunctions 12 | { 13 | /// 14 | /// Helpers 15 | /// 16 | internal static class Helpers 17 | { 18 | internal static RuleFunc ToResultTree(ReSettings reSettings, Rule rule, IEnumerable childRuleResults, Func isSuccessFunc, string exceptionMessage = "") 19 | { 20 | return (inputs) => { 21 | var isSuccess = false; 22 | var inputsDict = new Dictionary(); 23 | string finalMessage = exceptionMessage; 24 | try 25 | { 26 | inputsDict = inputs.ToDictionary(c => c.Name, c => c.Value); 27 | isSuccess = isSuccessFunc(inputs.Select(c => c.Value).ToArray()); 28 | } 29 | catch (Exception ex) 30 | { 31 | finalMessage = GetExceptionMessage($"Error while executing rule : {rule?.RuleName} - {ex.Message}", reSettings); 32 | HandleRuleException(new RuleException(exceptionMessage,ex), rule, reSettings); 33 | isSuccess = false; 34 | } 35 | 36 | return new RuleResultTree { 37 | Rule = rule, 38 | Inputs = inputsDict, 39 | IsSuccess = isSuccess, 40 | ChildResults = childRuleResults, 41 | ExceptionMessage = finalMessage 42 | }; 43 | 44 | }; 45 | } 46 | 47 | internal static RuleFunc ToRuleExceptionResult(ReSettings reSettings, Rule rule,Exception ex) 48 | { 49 | HandleRuleException(ex, rule, reSettings); 50 | return ToResultTree(reSettings, rule, null, (args) => false, ex.Message); 51 | } 52 | 53 | internal static void HandleRuleException(Exception ex, Rule rule, ReSettings reSettings) 54 | { 55 | ex.Data.Add(nameof(rule.RuleName), rule.RuleName); 56 | ex.Data.Add(nameof(rule.Expression), rule.Expression); 57 | 58 | if (!reSettings.EnableExceptionAsErrorMessage) 59 | { 60 | throw ex; 61 | } 62 | } 63 | 64 | /// 65 | /// 66 | /// 67 | /// 68 | /// 69 | /// 70 | /// 71 | /// 72 | internal static string GetExceptionMessage(string message,ReSettings reSettings) 73 | { 74 | return reSettings.IgnoreException ? "" : message; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/RulesEngine/HelperFunctions/MemCache.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Concurrent; 7 | using System.Collections.Generic; 8 | 9 | namespace RulesEngine.HelperFunctions 10 | { 11 | public class MemCacheConfig { 12 | public int SizeLimit { get; set; } = 1000; 13 | } 14 | 15 | 16 | internal class MemCache 17 | { 18 | private readonly MemCacheConfig _config; 19 | private ConcurrentDictionary _cacheDictionary; 20 | private ConcurrentQueue<(string key, DateTimeOffset expiry)> _cacheEvictionQueue; 21 | 22 | public MemCache(MemCacheConfig config) 23 | { 24 | if(config == null) 25 | { 26 | config = new MemCacheConfig(); 27 | } 28 | _config = config; 29 | _cacheDictionary = new ConcurrentDictionary(); 30 | _cacheEvictionQueue = new ConcurrentQueue<(string key, DateTimeOffset expiry)>(); 31 | } 32 | 33 | public bool TryGetValue(string key,out T value) 34 | { 35 | value = default; 36 | if (_cacheDictionary.TryGetValue(key, out var cacheItem)) 37 | { 38 | if(cacheItem.expiry < DateTimeOffset.UtcNow) 39 | { 40 | _cacheDictionary.TryRemove(key, out _); 41 | return false; 42 | } 43 | else 44 | { 45 | value = (T)cacheItem.value; 46 | return true; 47 | } 48 | } 49 | return false; 50 | 51 | } 52 | 53 | 54 | public T Get(string key) 55 | { 56 | TryGetValue(key, out var value); 57 | return value; 58 | } 59 | 60 | 61 | /// 62 | /// Returns all known keys. May return keys for expired data as well 63 | /// 64 | /// 65 | public IEnumerable GetKeys() 66 | { 67 | return _cacheDictionary.Keys; 68 | } 69 | 70 | public T GetOrCreate(string key, Func createFn, DateTimeOffset? expiry = null) 71 | { 72 | if(!TryGetValue(key,out var value)) 73 | { 74 | value = createFn(); 75 | return Set(key,value,expiry); 76 | } 77 | return value; 78 | } 79 | 80 | public T Set(string key, T value, DateTimeOffset? expiry = null) 81 | { 82 | var fixedExpiry = expiry ?? DateTimeOffset.MaxValue; 83 | 84 | while (_cacheDictionary.Count > _config.SizeLimit) 85 | { 86 | if (_cacheEvictionQueue.IsEmpty) 87 | { 88 | _cacheDictionary.Clear(); 89 | } 90 | if(_cacheEvictionQueue.TryDequeue(out var result) 91 | && _cacheDictionary.TryGetValue(result.key,out var dictionaryValue) 92 | && dictionaryValue.expiry == result.expiry) 93 | { 94 | _cacheDictionary.TryRemove(result.key, out _); 95 | } 96 | 97 | } 98 | 99 | _cacheDictionary.AddOrUpdate(key, (value, fixedExpiry), (k, v) => (value, fixedExpiry)); 100 | _cacheEvictionQueue.Enqueue((key, fixedExpiry)); 101 | return value; 102 | } 103 | 104 | public void Remove(string key) 105 | { 106 | _cacheDictionary.TryRemove(key, out _); 107 | } 108 | 109 | public void Clear() 110 | { 111 | _cacheDictionary.Clear(); 112 | _cacheEvictionQueue = new ConcurrentQueue<(string key, DateTimeOffset expiry)>(); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/RulesEngine/HelperFunctions/Utils.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Dynamic; 8 | using System.Linq; 9 | using System.Linq.Dynamic.Core; 10 | 11 | namespace RulesEngine.HelperFunctions 12 | { 13 | public static class Utils 14 | { 15 | public static object GetTypedObject(dynamic input) 16 | { 17 | if (input is ExpandoObject) 18 | { 19 | Type type = CreateAbstractClassType(input); 20 | return CreateObject(type, input); 21 | } 22 | else 23 | { 24 | return input; 25 | } 26 | } 27 | public static Type CreateAbstractClassType(dynamic input) 28 | { 29 | List props = []; 30 | 31 | if (input is System.Text.Json.JsonElement jsonElement) 32 | { 33 | if (jsonElement.ValueKind == System.Text.Json.JsonValueKind.Null) 34 | { 35 | return typeof(object); 36 | } 37 | } 38 | else if (input == null) 39 | { 40 | return typeof(object); 41 | } 42 | 43 | if (input is not ExpandoObject expandoObject) 44 | { 45 | return input.GetType(); 46 | } 47 | 48 | foreach (var expando in expandoObject) 49 | { 50 | Type value; 51 | if (expando.Value is IList list) 52 | { 53 | if (list.Count == 0) 54 | { 55 | value = typeof(List); 56 | } 57 | else 58 | { 59 | var internalType = CreateAbstractClassType(list[0]); 60 | value = new List().Cast(internalType).ToList(internalType).GetType(); 61 | } 62 | 63 | } 64 | else 65 | { 66 | value = CreateAbstractClassType(expando.Value); 67 | } 68 | props.Add(new DynamicProperty(expando.Key, value)); 69 | } 70 | 71 | var type = DynamicClassFactory.CreateType(props); 72 | return type; 73 | } 74 | 75 | public static object CreateObject(Type type, dynamic input) 76 | { 77 | if (input is not ExpandoObject expandoObject) 78 | { 79 | return Convert.ChangeType(input, type); 80 | } 81 | var obj = Activator.CreateInstance(type); 82 | 83 | var typeProps = type.GetProperties().ToDictionary(c => c.Name); 84 | 85 | foreach (var expando in expandoObject) 86 | { 87 | if (typeProps.ContainsKey(expando.Key) && 88 | expando.Value != null && (expando.Value.GetType().Name != "DBNull" || expando.Value != DBNull.Value)) 89 | { 90 | object val; 91 | var propInfo = typeProps[expando.Key]; 92 | if (expando.Value is ExpandoObject) 93 | { 94 | var propType = propInfo.PropertyType; 95 | val = CreateObject(propType, expando.Value); 96 | } 97 | else if (expando.Value is IList temp) 98 | { 99 | var internalType = propInfo.PropertyType.GenericTypeArguments.FirstOrDefault() ?? typeof(object); 100 | var newList = new List().Cast(internalType).ToList(internalType); 101 | foreach (var t in temp) 102 | { 103 | var child = CreateObject(internalType, t); 104 | newList.Add(child); 105 | }; 106 | val = newList; 107 | } 108 | else 109 | { 110 | val = expando.Value; 111 | } 112 | propInfo.SetValue(obj, val, null); 113 | } 114 | } 115 | 116 | return obj; 117 | } 118 | 119 | private static IEnumerable Cast(this IEnumerable self, Type innerType) 120 | { 121 | var methodInfo = typeof(Enumerable).GetMethod("Cast"); 122 | var genericMethod = methodInfo.MakeGenericMethod(innerType); 123 | return genericMethod.Invoke(null, new[] { self }) as IEnumerable; 124 | } 125 | 126 | private static IList ToList(this IEnumerable self, Type innerType) 127 | { 128 | var methodInfo = typeof(Enumerable).GetMethod("ToList"); 129 | var genericMethod = methodInfo.MakeGenericMethod(innerType); 130 | return genericMethod.Invoke(null, new[] { self }) as IList; 131 | } 132 | } 133 | 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/RulesEngine/Interfaces/IRulesEngine.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.Models; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | 8 | namespace RulesEngine.Interfaces 9 | { 10 | public interface IRulesEngine 11 | { 12 | /// 13 | /// This will execute all the rules of the specified workflow 14 | /// 15 | /// The name of the workflow with rules to execute against the inputs 16 | /// A variable number of inputs 17 | /// List of rule results 18 | ValueTask> ExecuteAllRulesAsync(string workflowName, params object[] inputs); 19 | 20 | /// 21 | /// This will execute all the rules of the specified workflow 22 | /// 23 | /// The name of the workflow with rules to execute against the inputs 24 | /// A variable number of rule parameters 25 | /// List of rule results 26 | ValueTask> ExecuteAllRulesAsync(string workflowName, params RuleParameter[] ruleParams); 27 | ValueTask ExecuteActionWorkflowAsync(string workflowName, string ruleName, RuleParameter[] ruleParameters); 28 | 29 | /// 30 | /// Adds new workflows to RulesEngine 31 | /// 32 | /// 33 | void AddWorkflow(params Workflow[] Workflows); 34 | 35 | /// 36 | /// Removes all registered workflows from RulesEngine 37 | /// 38 | void ClearWorkflows(); 39 | 40 | /// 41 | /// Removes the workflow from RulesEngine 42 | /// 43 | /// 44 | void RemoveWorkflow(params string[] workflowNames); 45 | 46 | /// 47 | /// Checks is workflow exist. 48 | /// 49 | /// The workflow name. 50 | /// true if contains the specified workflow name; otherwise, false. 51 | bool ContainsWorkflow(string workflowName); 52 | 53 | /// 54 | /// Returns the list of all registered workflow names 55 | /// 56 | /// 57 | List GetAllRegisteredWorkflowNames(); 58 | void AddOrUpdateWorkflow(params Workflow[] Workflows); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/RulesEngine/Models/ActionInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections.Generic; 5 | using System.Diagnostics.CodeAnalysis; 6 | 7 | namespace RulesEngine.Models 8 | { 9 | [ExcludeFromCodeCoverage] 10 | public class ActionInfo 11 | { 12 | public string Name { get; set; } 13 | public Dictionary Context { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/RulesEngine/Models/ActionResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Diagnostics.CodeAnalysis; 6 | 7 | namespace RulesEngine.Models 8 | { 9 | [ExcludeFromCodeCoverage] 10 | public class ActionResult 11 | { 12 | public object Output { get; set; } 13 | public Exception Exception { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/RulesEngine/Models/ActionRuleResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections.Generic; 5 | using System.Diagnostics.CodeAnalysis; 6 | 7 | namespace RulesEngine.Models 8 | { 9 | [ExcludeFromCodeCoverage] 10 | public class ActionRuleResult : ActionResult 11 | { 12 | public List Results { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/RulesEngine/Models/ReSettings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.Actions; 5 | using RulesEngine.HelperFunctions; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Diagnostics.CodeAnalysis; 9 | 10 | namespace RulesEngine.Models 11 | { 12 | [ExcludeFromCodeCoverage] 13 | public class ReSettings 14 | { 15 | public ReSettings() { } 16 | 17 | // create a copy of settings 18 | internal ReSettings(ReSettings reSettings) 19 | { 20 | CustomTypes = reSettings.CustomTypes; 21 | CustomActions = reSettings.CustomActions; 22 | EnableExceptionAsErrorMessage = reSettings.EnableExceptionAsErrorMessage; 23 | IgnoreException = reSettings.IgnoreException; 24 | EnableFormattedErrorMessage = reSettings.EnableFormattedErrorMessage; 25 | EnableScopedParams = reSettings.EnableScopedParams; 26 | NestedRuleExecutionMode = reSettings.NestedRuleExecutionMode; 27 | CacheConfig = reSettings.CacheConfig; 28 | IsExpressionCaseSensitive = reSettings.IsExpressionCaseSensitive; 29 | AutoRegisterInputType = reSettings.AutoRegisterInputType; 30 | UseFastExpressionCompiler = reSettings.UseFastExpressionCompiler; 31 | EnableExceptionAsErrorMessageForRuleExpressionParsing = reSettings.EnableExceptionAsErrorMessageForRuleExpressionParsing; 32 | } 33 | 34 | 35 | /// 36 | /// Get/Set the custom types to be used in Rule expressions 37 | /// 38 | public Type[] CustomTypes { get; set; } 39 | 40 | /// 41 | /// Get/Set the custom actions that can be used in the Rules 42 | /// 43 | public Dictionary> CustomActions { get; set; } 44 | 45 | /// 46 | /// When set to true, returns any exception occurred 47 | /// while rule execution as ErrorMessage 48 | /// otherwise throws an exception 49 | /// 50 | /// This setting is only applicable if IgnoreException is set to false 51 | public bool EnableExceptionAsErrorMessage { get; set; } = true; 52 | 53 | /// 54 | /// When set to true, it will ignore any exception thrown with rule compilation/execution 55 | /// 56 | public bool IgnoreException { get; set; } = false; 57 | 58 | /// 59 | /// Enables ErrorMessage Formatting 60 | /// 61 | public bool EnableFormattedErrorMessage { get; set; } = true; 62 | 63 | /// 64 | /// Enables Global params and local params for rules 65 | /// 66 | public bool EnableScopedParams { get; set; } = true; 67 | 68 | /// 69 | /// Sets whether expression are case sensitive 70 | /// 71 | public bool IsExpressionCaseSensitive { get; set; } = false; 72 | 73 | /// 74 | /// Auto Registers input type in Custom Type to allow calling method on type. 75 | /// Default : true 76 | /// 77 | public bool AutoRegisterInputType { get; set; } = true; 78 | 79 | /// 80 | /// Sets the mode for Nested rule execution, Default: All 81 | /// 82 | public NestedRuleExecutionMode NestedRuleExecutionMode { get; set; } = NestedRuleExecutionMode.All; 83 | public MemCacheConfig CacheConfig { get; set; } 84 | /// 85 | /// Whether to use FastExpressionCompiler for rule compilation 86 | /// 87 | public bool UseFastExpressionCompiler { get; set; } = false; 88 | /// 89 | /// Sets the mode for ParsingException to cascade to child elements and result in a expression parser 90 | /// Default: true 91 | /// 92 | public bool EnableExceptionAsErrorMessageForRuleExpressionParsing { get; set; } = true; 93 | } 94 | 95 | public enum NestedRuleExecutionMode 96 | { 97 | /// 98 | /// Executes all nested rules 99 | /// 100 | All, 101 | /// 102 | /// Skips nested rules whose execution does not impact parent rule's result 103 | /// 104 | Performance 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/RulesEngine/Models/Rule.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections.Generic; 5 | using System.Diagnostics.CodeAnalysis; 6 | 7 | namespace RulesEngine.Models 8 | { 9 | using System.Text.Json.Serialization; 10 | 11 | /// 12 | /// Rule class 13 | /// 14 | [ExcludeFromCodeCoverage] 15 | public class Rule 16 | { 17 | /// 18 | /// Rule name for the Rule 19 | /// 20 | public string RuleName { get; set; } 21 | /// 22 | /// Gets or sets the custom property or tags of the rule. 23 | /// 24 | /// 25 | /// The properties of the rule. 26 | /// 27 | public Dictionary Properties { get; set; } 28 | public string Operator { get; set; } 29 | public string ErrorMessage { get; set; } 30 | 31 | /// 32 | /// Gets or sets whether the rule is enabled. 33 | /// 34 | public bool Enabled { get; set; } = true; 35 | 36 | [JsonConverter (typeof(JsonStringEnumConverter))] 37 | public RuleExpressionType RuleExpressionType { get; set; } = RuleExpressionType.LambdaExpression; 38 | public IEnumerable WorkflowsToInject { get; set; } 39 | public IEnumerable Rules { get; set; } 40 | public IEnumerable LocalParams { get; set; } 41 | public string Expression { get; set; } 42 | public RuleActions Actions { get; set; } 43 | public string SuccessEvent { get; set; } 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/RulesEngine/Models/RuleActions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Diagnostics.CodeAnalysis; 6 | 7 | namespace RulesEngine.Models 8 | { 9 | [Obsolete("RuleAction class is deprecated. Use RuleActions class instead.")] 10 | [ExcludeFromCodeCoverage] 11 | public class RuleAction : RuleActions 12 | { 13 | } 14 | 15 | [ExcludeFromCodeCoverage] 16 | public class RuleActions 17 | { 18 | public ActionInfo OnSuccess { get; set; } 19 | public ActionInfo OnFailure { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/RulesEngine/Models/RuleDelegate.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | namespace RulesEngine.Models 5 | { 6 | public delegate T RuleFunc(params RuleParameter[] ruleParameters); 7 | } 8 | -------------------------------------------------------------------------------- /src/RulesEngine/Models/RuleErrorType.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | namespace RulesEngine.Models 5 | { 6 | /// 7 | /// This is error type of rules which will use in rule config files 8 | /// 9 | public enum ErrorType 10 | { 11 | Warning = 0, 12 | Error = 1, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/RulesEngine/Models/RuleExpressionParameter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Diagnostics.CodeAnalysis; 6 | using System.Linq.Expressions; 7 | 8 | namespace RulesEngine.Models 9 | { 10 | /// 11 | /// CompiledParam class. 12 | /// 13 | [ExcludeFromCodeCoverage] 14 | public class RuleExpressionParameter 15 | { 16 | public ParameterExpression ParameterExpression { get; set; } 17 | 18 | public Expression ValueExpression { get; set; } 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/RulesEngine/Models/RuleExpressionType.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | namespace RulesEngine.Models 5 | { 6 | /// 7 | /// This is rule expression type which will use in rule config files 8 | /// 9 | public enum RuleExpressionType 10 | { 11 | LambdaExpression = 0 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/RulesEngine/Models/RuleParameter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.HelperFunctions; 5 | using System; 6 | using System.Diagnostics.CodeAnalysis; 7 | using System.Linq.Expressions; 8 | 9 | namespace RulesEngine.Models 10 | { 11 | [ExcludeFromCodeCoverage] 12 | public class RuleParameter 13 | { 14 | public RuleParameter(string name, object value) 15 | { 16 | Value = Utils.GetTypedObject(value); 17 | Init(name, Value?.GetType()); 18 | } 19 | 20 | 21 | internal RuleParameter(string name, Type type,object value = null) 22 | { 23 | Value = Utils.GetTypedObject(value); 24 | Init(name, type); 25 | } 26 | 27 | public Type Type { get; private set; } 28 | public string Name { get; private set; } 29 | public object Value { get; private set; } 30 | public ParameterExpression ParameterExpression { get; private set; } 31 | 32 | private void Init(string name, Type type) 33 | { 34 | Name = name; 35 | Type = type ?? typeof(object); 36 | ParameterExpression = Expression.Parameter(Type, Name); 37 | } 38 | 39 | public static RuleParameter Create(string name, Type type) 40 | { 41 | return new RuleParameter(name, type); 42 | } 43 | 44 | public static RuleParameter Create(string name, T value) 45 | { 46 | var typedValue = Utils.GetTypedObject(value); 47 | var type = typedValue?.GetType() ?? typeof(T); 48 | return new RuleParameter(name,type,value); 49 | } 50 | 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/RulesEngine/Models/RuleResultTree.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.HelperFunctions; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Diagnostics.CodeAnalysis; 8 | 9 | namespace RulesEngine.Models 10 | { 11 | /// 12 | /// Rule result class with child result heirarchy 13 | /// 14 | [ExcludeFromCodeCoverage] 15 | public class RuleResultTree 16 | { 17 | /// 18 | /// Gets or sets the rule. 19 | /// 20 | /// 21 | /// The rule. 22 | /// 23 | public Rule Rule { get; set; } 24 | 25 | /// 26 | /// Gets or sets a value indicating whether this instance is success. 27 | /// 28 | /// 29 | /// true if this instance is success; otherwise, false. 30 | /// 31 | public bool IsSuccess { get; set; } 32 | 33 | /// 34 | /// Gets or sets the child result. 35 | /// 36 | /// 37 | /// The child result. 38 | /// 39 | public IEnumerable ChildResults { get; set; } 40 | 41 | /// 42 | /// Gets or sets the input object 43 | /// 44 | public Dictionary Inputs { get; set; } 45 | 46 | public ActionResult ActionResult { get; set; } 47 | 48 | /// 49 | /// Gets the exception message in case an error is thrown during rules calculation. 50 | /// 51 | public string ExceptionMessage { get; set; } 52 | 53 | } 54 | 55 | /// 56 | /// This class will hold the error messages 57 | /// 58 | [ExcludeFromCodeCoverage] 59 | public class RuleResultMessage 60 | { 61 | /// 62 | /// Constructor will initialize the List 63 | /// 64 | public RuleResultMessage() 65 | { 66 | ErrorMessages = new List(); 67 | WarningMessages = new List(); 68 | } 69 | 70 | /// 71 | /// This will hold the list of error messages 72 | /// 73 | public List ErrorMessages { get; set; } 74 | 75 | /// 76 | /// This will hold the list of warning messages 77 | /// 78 | public List WarningMessages { get; set; } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/RulesEngine/Models/ScopedParam.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System.Diagnostics.CodeAnalysis; 5 | 6 | namespace RulesEngine.Models 7 | { 8 | /// Class LocalParam. 9 | /// 10 | [ExcludeFromCodeCoverage] 11 | public class ScopedParam 12 | { 13 | 14 | /// 15 | /// Gets or sets the name of the param. 16 | /// 17 | /// 18 | /// The name of the rule. 19 | /// ] 20 | public string Name { get; set; } 21 | 22 | /// 23 | /// Gets or Sets the lambda expression which can be reference in Rule. 24 | /// 25 | public string Expression { get; set; } 26 | } 27 | 28 | [ExcludeFromCodeCoverage] 29 | public class LocalParam : ScopedParam { } 30 | } 31 | -------------------------------------------------------------------------------- /src/RulesEngine/Models/Workflow.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | namespace RulesEngine.Models 9 | { 10 | [Obsolete("WorkflowRules class is deprecated. Use Workflow class instead.")] 11 | [ExcludeFromCodeCoverage] 12 | public class WorkflowRules : Workflow { 13 | } 14 | 15 | /// 16 | /// Workflow rules class for deserialization the json config file 17 | /// 18 | [ExcludeFromCodeCoverage] 19 | public class Workflow 20 | { 21 | /// 22 | /// Gets the workflow name. 23 | /// 24 | public string WorkflowName { get; set; } 25 | 26 | /// Gets or sets the workflow rules to inject. 27 | /// The workflow rules to inject. 28 | [Obsolete("WorkflowRulesToInject is deprecated. Use WorkflowsToInject instead.")] 29 | public IEnumerable WorkflowRulesToInject { 30 | set { WorkflowsToInject = value; } 31 | } 32 | public IEnumerable WorkflowsToInject { get; set; } 33 | 34 | public RuleExpressionType RuleExpressionType { get; set; } = RuleExpressionType.LambdaExpression; 35 | 36 | /// 37 | /// Gets or Sets the global params which will be applicable to all rules 38 | /// 39 | public IEnumerable GlobalParams { get; set; } 40 | 41 | /// 42 | /// list of rules. 43 | /// 44 | public IEnumerable Rules { get; set; } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/RulesEngine/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using System.Runtime.CompilerServices; 5 | using System.Runtime.InteropServices; 6 | 7 | // Setting ComVisible to false makes the types in this assembly not visible 8 | // to COM components. If you need to access a type in this assembly from 9 | // COM, set the ComVisible attribute to true on that type. 10 | [assembly: ComVisible(false)] 11 | [assembly: InternalsVisibleTo("RulesEngine.UnitTest, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c15956b2ac0945c55b69a185f5c3e02276693b0a5e42c8a1f08cb24e03dd87d91f9fa09f79b6b7b3aac4df46f2ea4ce4bfa31920bb0aad9f02793ab29de9fbf40f5ba9e347aa8569128459f31da1f6357eabe6e1308ac7c16b87a4d61e8d1785746a57ec67956d2e2454b3c98502a5d5c4a4168133bfaa431207c108efae03aa")] 12 | -------------------------------------------------------------------------------- /src/RulesEngine/RuleExpressionBuilderFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.ExpressionBuilders; 5 | using RulesEngine.Models; 6 | using System; 7 | 8 | namespace RulesEngine 9 | { 10 | internal class RuleExpressionBuilderFactory 11 | { 12 | private readonly ReSettings _reSettings; 13 | private readonly LambdaExpressionBuilder _lambdaExpressionBuilder; 14 | public RuleExpressionBuilderFactory(ReSettings reSettings, RuleExpressionParser expressionParser) 15 | { 16 | _reSettings = reSettings; 17 | _lambdaExpressionBuilder = new LambdaExpressionBuilder(_reSettings, expressionParser); 18 | } 19 | public RuleExpressionBuilderBase RuleGetExpressionBuilder(RuleExpressionType ruleExpressionType) 20 | { 21 | switch (ruleExpressionType) 22 | { 23 | case RuleExpressionType.LambdaExpression: 24 | return _lambdaExpressionBuilder; 25 | default: 26 | throw new InvalidOperationException($"{nameof(ruleExpressionType)} has not been supported yet."); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/RulesEngine/RulesCache.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.HelperFunctions; 5 | using RulesEngine.Models; 6 | using System; 7 | using System.Collections.Concurrent; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | 11 | namespace RulesEngine 12 | { 13 | /// Class RulesCache. 14 | internal class RulesCache 15 | { 16 | /// The compile rules 17 | private readonly MemCache _compileRules; 18 | 19 | /// The workflow rules 20 | private readonly ConcurrentDictionary _workflow = new ConcurrentDictionary(); 21 | 22 | 23 | public RulesCache(ReSettings reSettings) 24 | { 25 | _compileRules = new MemCache(reSettings.CacheConfig); 26 | } 27 | 28 | 29 | /// Determines whether [contains workflow rules] [the specified workflow name]. 30 | /// Name of the workflow. 31 | /// 32 | /// true if [contains workflow rules] [the specified workflow name]; otherwise, false. 33 | public bool ContainsWorkflows(string workflowName) 34 | { 35 | return _workflow.ContainsKey(workflowName); 36 | } 37 | 38 | public List GetAllWorkflowNames() 39 | { 40 | return _workflow.Keys.ToList(); 41 | } 42 | 43 | /// Adds the or update workflow rules. 44 | /// Name of the workflow. 45 | /// The rules. 46 | public void AddOrUpdateWorkflows(string workflowName, Workflow rules) 47 | { 48 | long ticks = DateTime.UtcNow.Ticks; 49 | _workflow.AddOrUpdate(workflowName, (rules, ticks), (k, v) => (rules, ticks)); 50 | } 51 | 52 | /// Adds the or update compiled rule. 53 | /// The compiled rule key. 54 | /// The compiled rule. 55 | public void AddOrUpdateCompiledRule(string compiledRuleKey, IDictionary> compiledRule) 56 | { 57 | long ticks = DateTime.UtcNow.Ticks; 58 | _compileRules.Set(compiledRuleKey,(compiledRule, ticks)); 59 | } 60 | 61 | /// Checks if the compiled rules are up-to-date. 62 | /// The compiled rule key. 63 | /// The workflow name. 64 | /// 65 | /// true if [compiled rules] is newer than the [workflow rules]; otherwise, false. 66 | public bool AreCompiledRulesUpToDate(string compiledRuleKey, string workflowName) 67 | { 68 | if (_compileRules.TryGetValue(compiledRuleKey, out (IDictionary> rules, long tick) compiledRulesObj)) 69 | { 70 | if (_workflow.TryGetValue(workflowName, out (Workflow rules, long tick) WorkflowsObj)) 71 | { 72 | return compiledRulesObj.tick >= WorkflowsObj.tick; 73 | } 74 | } 75 | 76 | return false; 77 | } 78 | 79 | /// Clears this instance. 80 | public void Clear() 81 | { 82 | _workflow.Clear(); 83 | _compileRules.Clear(); 84 | } 85 | 86 | /// Gets the work flow rules. 87 | /// Name of the workflow. 88 | /// Workflows. 89 | /// Could not find injected Workflow: {wfname} 90 | public Workflow GetWorkflow(string workflowName) 91 | { 92 | if (_workflow.TryGetValue(workflowName, out (Workflow rules, long tick) WorkflowsObj)) 93 | { 94 | var workflow = WorkflowsObj.rules; 95 | if (workflow.WorkflowsToInject?.Any() == true) 96 | { 97 | if (workflow.Rules == null) 98 | { 99 | workflow.Rules = new List(); 100 | } 101 | foreach (string wfname in workflow.WorkflowsToInject) 102 | { 103 | var injectedWorkflow = GetWorkflow(wfname); 104 | if (injectedWorkflow == null) 105 | { 106 | throw new Exception($"Could not find injected Workflow: {wfname}"); 107 | } 108 | 109 | workflow.Rules = workflow.Rules.Concat(injectedWorkflow.Rules).ToList(); 110 | } 111 | } 112 | 113 | return workflow; 114 | } 115 | else 116 | { 117 | return null; 118 | } 119 | } 120 | 121 | 122 | /// Gets the compiled rules. 123 | /// The compiled rules key. 124 | /// CompiledRule. 125 | public IDictionary> GetCompiledRules(string compiledRulesKey) 126 | { 127 | return _compileRules.Get<(IDictionary> rules, long tick)>(compiledRulesKey).rules; 128 | } 129 | 130 | /// Removes the specified workflow name. 131 | /// Name of the workflow. 132 | public void Remove(string workflowName) 133 | { 134 | if (_workflow.TryRemove(workflowName, out var workflowObj)) 135 | { 136 | var compiledKeysToRemove = _compileRules.GetKeys().Where(key => key.StartsWith(workflowName)); 137 | foreach (var key in compiledKeysToRemove) 138 | { 139 | _compileRules.Remove(key); 140 | } 141 | } 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/RulesEngine/RulesEngine.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0;net8.0;net9.0;netstandard2.0 5 | 13.0 6 | 6.0.0 7 | Copyright (c) Microsoft Corporation. 8 | LICENSE 9 | https://github.com/microsoft/RulesEngine 10 | Purunjay Bhal 11 | 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. 12 | https://github.com/microsoft/RulesEngine/blob/main/CHANGELOG.md 13 | BRE, Rules Engine, Abstraction 14 | README.md 15 | 16 | 17 | 18 | true 19 | 20 | 21 | true 22 | true 23 | snupkg 24 | True 25 | ..\..\signing\RulesEngine-publicKey.snk 26 | True 27 | true 28 | true 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/RulesEngine/Validators/RuleValidator.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using FluentValidation; 5 | using RulesEngine.HelperFunctions; 6 | using RulesEngine.Models; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Linq.Expressions; 11 | 12 | namespace RulesEngine.Validators 13 | { 14 | internal class RuleValidator : AbstractValidator 15 | { 16 | private readonly List _nestedOperators = new List { ExpressionType.And, ExpressionType.AndAlso, ExpressionType.Or, ExpressionType.OrElse }; 17 | public RuleValidator() 18 | { 19 | RuleFor(c => c.RuleName).NotEmpty().WithMessage(Constants.RULE_NAME_NULL_ERRMSG); 20 | 21 | //Nested expression check 22 | When(c => c.Operator != null, () => { 23 | RuleFor(c => c.Operator) 24 | .NotNull().WithMessage(Constants.OPERATOR_NULL_ERRMSG) 25 | .Must(op => _nestedOperators.Any(x => x.ToString().Equals(op, StringComparison.OrdinalIgnoreCase))) 26 | .WithMessage(Constants.OPERATOR_INCORRECT_ERRMSG); 27 | 28 | When(c => c.Rules?.Any() != true, () => { 29 | RuleFor(c => c.WorkflowsToInject).NotEmpty().WithMessage(Constants.INJECT_WORKFLOW_RULES_ERRMSG); 30 | }) 31 | .Otherwise(() => { 32 | RuleFor(c => c.Rules).Must(BeValidRulesList); 33 | }); 34 | }); 35 | RegisterExpressionTypeRules(); 36 | } 37 | 38 | private void RegisterExpressionTypeRules() 39 | { 40 | When(c => c.Operator == null && c.RuleExpressionType == RuleExpressionType.LambdaExpression, () => { 41 | RuleFor(c => c.Expression).NotEmpty().WithMessage(Constants.LAMBDA_EXPRESSION_EXPRESSION_NULL_ERRMSG); 42 | RuleFor(c => c.Rules).Empty().WithMessage(Constants.OPERATOR_RULES_ERRMSG); 43 | }); 44 | } 45 | 46 | private bool BeValidRulesList(IEnumerable rules) 47 | { 48 | if (rules?.Any() != true) return false; 49 | var validator = new RuleValidator(); 50 | var isValid = true; 51 | foreach (var rule in rules) 52 | { 53 | isValid &= validator.Validate(rule).IsValid; 54 | if (!isValid) break; 55 | } 56 | return isValid; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/RulesEngine/Validators/WorkflowRulesValidator.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using FluentValidation; 5 | using RulesEngine.HelperFunctions; 6 | using RulesEngine.Models; 7 | using System.Linq; 8 | 9 | namespace RulesEngine.Validators 10 | { 11 | internal class WorkflowsValidator : AbstractValidator 12 | { 13 | public WorkflowsValidator() 14 | { 15 | RuleFor(c => c.WorkflowName).NotEmpty().WithMessage(Constants.WORKFLOW_NAME_NULL_ERRMSG); 16 | When(c => c.Rules?.Any() != true, () => { 17 | RuleFor(c => c.WorkflowsToInject).NotEmpty().WithMessage(Constants.INJECT_WORKFLOW_RULES_ERRMSG); 18 | }).Otherwise(() => { 19 | var ruleValidator = new RuleValidator(); 20 | RuleForEach(c => c.Rules).SetValidator(ruleValidator); 21 | }); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/ActionTests/ActionContextTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using AutoFixture; 5 | using RulesEngine.Actions; 6 | using RulesEngine.Models; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Diagnostics.CodeAnalysis; 10 | using Xunit; 11 | 12 | namespace RulesEngine.UnitTest 13 | { 14 | [ExcludeFromCodeCoverage] 15 | public class ActionContextTests 16 | { 17 | [Fact] 18 | public void GetParentRuleResult_ReturnsParentRule() 19 | { 20 | // Arrange 21 | var fixture = new Fixture(); 22 | var contextInput = fixture.Create(); 23 | var context = new Dictionary { 24 | { nameof(contextInput), contextInput } 25 | }; 26 | var parentRuleResult = new RuleResultTree(); 27 | 28 | var actionContext = new ActionContext(context, parentRuleResult); 29 | 30 | // Act 31 | var result = actionContext.GetParentRuleResult(); 32 | 33 | // Assert 34 | Assert.NotNull(result); 35 | Assert.Equal(parentRuleResult, result); 36 | } 37 | 38 | [Fact] 39 | public void GetContext_ValidName_ReturnsContext() 40 | { 41 | // Arrange 42 | var fixture = new Fixture(); 43 | var contextInput = fixture.Create(); 44 | var context = new Dictionary { 45 | { nameof(contextInput), contextInput } 46 | }; 47 | var parentRuleResult = new RuleResultTree(); 48 | 49 | var actionContext = new ActionContext(context, parentRuleResult); 50 | string name = nameof(contextInput); 51 | 52 | // Act 53 | var result = actionContext.GetContext(name); 54 | 55 | // Assert 56 | Assert.Equal(contextInput, result); 57 | } 58 | 59 | [Fact] 60 | public void GetContext_ObjectContext_ReturnsTypedContext() 61 | { 62 | // Arrange 63 | var fixture = new Fixture(); 64 | var contextInput = fixture.CreateMany(); 65 | var context = new Dictionary { 66 | { nameof(contextInput), contextInput } 67 | }; 68 | var parentRuleResult = new RuleResultTree(); 69 | 70 | 71 | var actionContext = new ActionContext(context, parentRuleResult); 72 | string name = nameof(contextInput); 73 | 74 | // Act 75 | var result = actionContext.GetContext>(name); 76 | 77 | // Assert 78 | Assert.Equal(contextInput, result); 79 | } 80 | 81 | [Fact] 82 | public void GetContext_ValidNameWithStringCaseDiffernce_ReturnsContext() 83 | { 84 | // Arrange 85 | var fixture = new Fixture(); 86 | var contextInput = fixture.Create(); 87 | var context = new Dictionary { 88 | { nameof(contextInput), contextInput } 89 | }; 90 | var parentRuleResult = new RuleResultTree(); 91 | 92 | var actionContext = new ActionContext(context, parentRuleResult); 93 | string name = nameof(contextInput).ToUpper(); 94 | 95 | // Act 96 | var result = actionContext.GetContext(name); 97 | 98 | // Assert 99 | Assert.Equal(contextInput, result); 100 | } 101 | 102 | [Fact] 103 | public void GetContext_InvalidName_ThrowsArgumentException() 104 | { 105 | // Arrange 106 | var fixture = new Fixture(); 107 | var contextInput = fixture.Create(); 108 | var context = new Dictionary { 109 | { nameof(contextInput), contextInput } 110 | }; 111 | var parentRuleResult = new RuleResultTree(); 112 | 113 | var actionContext = new ActionContext(context, parentRuleResult); 114 | string name = fixture.Create(); 115 | 116 | // Act 117 | Assert.Throws(() => actionContext.GetContext(name)); 118 | } 119 | 120 | [Fact] 121 | public void GetContext_PrimitiveInputs_ReturnsResult() 122 | { 123 | // Arrange 124 | var fixture = new Fixture(); 125 | var intInput = fixture.Create(); 126 | var strInput = fixture.Create(); 127 | var floatInput = fixture.Create(); 128 | 129 | var context = new Dictionary { 130 | { nameof(intInput), intInput }, 131 | { nameof(strInput), strInput }, 132 | { nameof(floatInput), floatInput }, 133 | }; 134 | var parentRuleResult = new RuleResultTree(); 135 | 136 | var actionContext = new ActionContext(context, parentRuleResult); 137 | 138 | // Act 139 | var intResult = actionContext.GetContext(nameof(intInput)); 140 | var strResult = actionContext.GetContext(nameof(strInput)); 141 | var floatResult = actionContext.GetContext(nameof(floatInput)); 142 | 143 | // Assert 144 | Assert.Equal(intInput, intResult); 145 | Assert.Equal(strInput, strResult); 146 | Assert.Equal(floatInput, floatResult); 147 | } 148 | 149 | [Fact] 150 | public void GetContext_InvalidNameListContext_ThrowsArgumentException() 151 | { 152 | // Arrange 153 | var fixture = new Fixture(); 154 | var contextInput = fixture.CreateMany(); 155 | var context = new Dictionary { 156 | { nameof(contextInput), contextInput } 157 | }; 158 | var parentRuleResult = new RuleResultTree(); 159 | 160 | var actionContext = new ActionContext(context, parentRuleResult); 161 | string name = fixture.Create(); 162 | 163 | // Act 164 | Assert.Throws(() => actionContext.GetContext>(name)); 165 | } 166 | 167 | [Fact] 168 | public void GetContext_InvalidTypeConversion_ThrowsArgumentException() 169 | { 170 | // Arrange 171 | var fixture = new Fixture(); 172 | var contextInput = fixture.CreateMany(); 173 | var context = new Dictionary { 174 | { nameof(contextInput), contextInput } 175 | }; 176 | var parentRuleResult = new RuleResultTree(); 177 | 178 | var actionContext = new ActionContext(context, parentRuleResult); 179 | string name = nameof(contextInput); 180 | 181 | // Act 182 | Assert.Throws(() => actionContext.GetContext(name)); 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/ActionTests/CustomActionTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Newtonsoft.Json; 5 | using RulesEngine.Models; 6 | using RulesEngine.UnitTest.ActionTests.MockClass; 7 | using System.Collections.Generic; 8 | using System.Diagnostics.CodeAnalysis; 9 | using System.Text.Json.Serialization; 10 | using System.Threading.Tasks; 11 | using Xunit; 12 | 13 | namespace RulesEngine.UnitTest.ActionTests 14 | { 15 | [ExcludeFromCodeCoverage] 16 | public class CustomActionTest 17 | { 18 | [Fact] 19 | public async Task CustomActionOnRuleMustHaveContextValues() 20 | { 21 | var workflow = GetWorkflow(); 22 | var re = new RulesEngine(workflow, reSettings: new ReSettings { 23 | CustomActions = new Dictionary> { 24 | 25 | { "ReturnContext", () => new ReturnContextAction() } 26 | } 27 | }); 28 | 29 | var result = await re.ExecuteAllRulesAsync("successReturnContextAction", true); 30 | } 31 | 32 | 33 | [Fact] 34 | public async Task CustomAction_WithSystemTextJsobOnRuleMustHaveContextValues() 35 | { 36 | var workflow = GetWorkflow(); 37 | var workflowStr = JsonConvert.SerializeObject(workflow); 38 | var serializationOptions = new System.Text.Json.JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } }; 39 | var workflowViaTextJson = System.Text.Json.JsonSerializer.Deserialize(workflowStr,serializationOptions); 40 | 41 | 42 | var re = new RulesEngine(workflow, reSettings: new ReSettings { 43 | CustomActions = new Dictionary> { 44 | 45 | { "ReturnContext", () => new ReturnContextAction() } 46 | } 47 | }); 48 | 49 | 50 | 51 | var result = await re.ExecuteAllRulesAsync("successReturnContextAction", true); 52 | } 53 | 54 | private Workflow[] GetWorkflow() 55 | { 56 | return new Workflow[] { 57 | new Workflow { 58 | WorkflowName = "successReturnContextAction", 59 | Rules = new Rule[] { 60 | new Rule { 61 | RuleName = "trueRule", 62 | Expression = "input1 == true", 63 | Actions = new RuleActions() { 64 | OnSuccess = new ActionInfo { 65 | Name = "ReturnContext", 66 | Context = new Dictionary { 67 | {"stringContext", "hello"}, 68 | {"intContext",1 }, 69 | {"objectContext", new { a = "hello", b = 123 } } 70 | } 71 | } 72 | 73 | } 74 | 75 | }, 76 | 77 | 78 | } 79 | } 80 | 81 | }; 82 | } 83 | 84 | 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/ActionTests/MockClass/ReturnContextAction.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.Actions; 5 | using RulesEngine.Models; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Diagnostics.CodeAnalysis; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | 12 | namespace RulesEngine.UnitTest.ActionTests.MockClass 13 | { 14 | [ExcludeFromCodeCoverage] 15 | public class ReturnContextAction : ActionBase 16 | { 17 | public override ValueTask Run(ActionContext context, RuleParameter[] ruleParameters) 18 | { 19 | var stringContext = context.GetContext("stringContext"); 20 | var intContext = context.GetContext("intContext"); 21 | var objectContext = context.GetContext("objectContext"); 22 | 23 | return new ValueTask(new { 24 | stringContext, 25 | intContext, 26 | objectContext 27 | }); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/CaseSensitiveTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.Models; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Diagnostics.CodeAnalysis; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using Xunit; 12 | 13 | namespace RulesEngine.UnitTest 14 | { 15 | [ExcludeFromCodeCoverage] 16 | public class CaseSensitiveTests 17 | { 18 | [Theory] 19 | [InlineData(true,true,false)] 20 | [InlineData(false,true,true)] 21 | public async Task CaseSensitiveTest(bool caseSensitive, bool expected1, bool expected2) 22 | { 23 | var reSettings = new ReSettings { 24 | IsExpressionCaseSensitive = caseSensitive 25 | }; 26 | 27 | 28 | var worflow = new Workflow { 29 | WorkflowName = "CaseSensitivityTest", 30 | Rules = new[] { 31 | new Rule { 32 | RuleName = "check same case1", 33 | Expression = "input1 == \"hello\"" 34 | }, 35 | new Rule { 36 | RuleName = "check same case2", 37 | Expression = "INPUT1 == \"hello\"" 38 | } 39 | } 40 | }; 41 | 42 | var re = new RulesEngine(new[] { worflow }, reSettings); 43 | var result = await re.ExecuteAllRulesAsync("CaseSensitivityTest", "hello"); 44 | 45 | Assert.Equal(expected1, result[0].IsSuccess); 46 | Assert.Equal(expected2, result[1].IsSuccess); 47 | } 48 | 49 | 50 | 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/CustomTypeProviderTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Moq; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Diagnostics.CodeAnalysis; 8 | using System.Linq; 9 | using Xunit; 10 | 11 | namespace RulesEngine.UnitTest 12 | { 13 | [Trait("Category", "Unit")] 14 | [ExcludeFromCodeCoverage] 15 | public class CustomTypeProviderTests : IDisposable 16 | { 17 | public void Dispose() 18 | { 19 | } 20 | 21 | private CustomTypeProvider CreateProvider(params Type[] customTypes) 22 | { 23 | return new CustomTypeProvider(customTypes); 24 | } 25 | 26 | [Fact] 27 | public void GetCustomTypes_DefaultProvider_IncludesEnumerableAndObject() 28 | { 29 | var provider = CreateProvider(); 30 | var allTypes = provider.GetCustomTypes(); 31 | Assert.NotEmpty(allTypes); 32 | Assert.Contains(typeof(System.Linq.Enumerable), allTypes); 33 | Assert.Contains(typeof(object), allTypes); 34 | } 35 | 36 | [Fact] 37 | public void GetCustomTypes_WithListOfGuid_ContainsIEnumerableOfGuid() 38 | { 39 | var initial = new[] { typeof(List) }; 40 | var provider = CreateProvider(initial); 41 | var allTypes = provider.GetCustomTypes(); 42 | Assert.Contains(typeof(IEnumerable), allTypes); 43 | Assert.Contains(typeof(List), allTypes); 44 | Assert.Contains(typeof(System.Linq.Enumerable), allTypes); 45 | Assert.Contains(typeof(object), allTypes); 46 | } 47 | 48 | [Fact] 49 | public void GetCustomTypes_ListOfListString_ContainsIEnumerableOfListString() 50 | { 51 | var nestedListType = typeof(List>); 52 | var provider = CreateProvider(nestedListType); 53 | var allTypes = provider.GetCustomTypes(); 54 | Assert.Contains(typeof(IEnumerable>), allTypes); 55 | Assert.Contains(nestedListType, allTypes); 56 | Assert.Contains(typeof(System.Linq.Enumerable), allTypes); 57 | Assert.Contains(typeof(object), allTypes); 58 | } 59 | 60 | [Fact] 61 | public void GetCustomTypes_ArrayOfStringArrays_ContainsIEnumerableOfStringArray() 62 | { 63 | var arrayType = typeof(string[][]); 64 | var provider = CreateProvider(arrayType); 65 | var allTypes = provider.GetCustomTypes(); 66 | Assert.Contains(typeof(IEnumerable), allTypes); 67 | Assert.Contains(arrayType, allTypes); 68 | Assert.Contains(typeof(System.Linq.Enumerable), allTypes); 69 | Assert.Contains(typeof(object), allTypes); 70 | } 71 | 72 | [Fact] 73 | public void GetCustomTypes_NullableIntArray_ContainsIEnumerableOfNullableInt() 74 | { 75 | var nullableInt = typeof(int?); 76 | var arrayType = typeof(int?[]); 77 | var provider = CreateProvider(arrayType); 78 | var allTypes = provider.GetCustomTypes(); 79 | Assert.Contains(typeof(IEnumerable), allTypes); 80 | Assert.Contains(arrayType, allTypes); 81 | Assert.Contains(typeof(System.Linq.Enumerable), allTypes); 82 | Assert.Contains(typeof(object), allTypes); 83 | } 84 | 85 | [Fact] 86 | public void GetCustomTypes_MultipleTypes_NoDuplicates() 87 | { 88 | var repeatedType = typeof(List); 89 | var provider = CreateProvider(repeatedType, repeatedType); 90 | var allTypes = provider.GetCustomTypes(); 91 | var matches = allTypes.Where(t => t == repeatedType).ToList(); 92 | Assert.Single(matches); 93 | var interfaceMatches = allTypes.Where(t => t == typeof(IEnumerable)).ToList(); 94 | Assert.Single(interfaceMatches); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/EmptyRulesTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Newtonsoft.Json; 5 | using RulesEngine.Models; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Diagnostics.CodeAnalysis; 9 | using System.Dynamic; 10 | using System.Linq; 11 | using System.Text.Json.Serialization; 12 | using System.Threading.Tasks; 13 | using Xunit; 14 | 15 | namespace RulesEngine.UnitTest 16 | { 17 | [ExcludeFromCodeCoverage] 18 | public class EmptyRulesTest 19 | { 20 | [Fact] 21 | private async Task EmptyRules_ReturnsExepectedResults() 22 | { 23 | var workflow = GetEmptyWorkflow(); 24 | var reSettings = new ReSettings { }; 25 | RulesEngine rulesEngine = new RulesEngine(); 26 | 27 | Func action = () => { 28 | new RulesEngine(workflow, reSettings: reSettings); 29 | return Task.CompletedTask; 30 | }; 31 | 32 | Exception ex = await Assert.ThrowsAsync(action); 33 | 34 | Assert.Contains("Atleast one of Rules or WorkflowsToInject must be not empty", ex.Message); 35 | } 36 | [Fact] 37 | private async Task NestedRulesWithEmptyNestedActions_ReturnsExepectedResults() 38 | { 39 | var workflow = GetEmptyNestedWorkflows(); 40 | var reSettings = new ReSettings { }; 41 | RulesEngine rulesEngine = new RulesEngine(); 42 | 43 | Func action = () => { 44 | new RulesEngine(workflow, reSettings: reSettings); 45 | return Task.CompletedTask; 46 | }; 47 | 48 | Exception ex = await Assert.ThrowsAsync(action); 49 | 50 | Assert.Contains("Atleast one of Rules or WorkflowsToInject must be not empty", ex.Message); 51 | } 52 | 53 | private Workflow[] GetEmptyWorkflow() 54 | { 55 | return new[] { 56 | new Workflow { 57 | WorkflowName = "EmptyRulesTest", 58 | Rules = new Rule[] { 59 | } 60 | } 61 | }; 62 | } 63 | 64 | private Workflow[] GetEmptyNestedWorkflows() 65 | { 66 | return new[] { 67 | new Workflow { 68 | WorkflowName = "EmptyNestedRulesTest", 69 | Rules = new Rule[] { 70 | new Rule { 71 | RuleName = "AndRuleTrueFalse", 72 | Operator = "And", 73 | Rules = new Rule[] { 74 | new Rule{ 75 | RuleName = "trueRule1", 76 | Expression = "input1.TrueValue == true", 77 | }, 78 | new Rule { 79 | RuleName = "falseRule1", 80 | Expression = "input1.TrueValue == false" 81 | } 82 | 83 | } 84 | }, 85 | new Rule { 86 | RuleName = "OrRuleTrueFalse", 87 | Operator = "Or", 88 | Rules = new Rule[] { 89 | new Rule{ 90 | RuleName = "trueRule2", 91 | Expression = "input1.TrueValue == true", 92 | }, 93 | new Rule { 94 | RuleName = "falseRule2", 95 | Expression = "input1.TrueValue == false" 96 | } 97 | 98 | } 99 | }, 100 | new Rule { 101 | RuleName = "AndRuleFalseTrue", 102 | Operator = "And", 103 | Rules = new Rule[] { 104 | new Rule{ 105 | RuleName = "trueRule3", 106 | Expression = "input1.TrueValue == false", 107 | }, 108 | new Rule { 109 | RuleName = "falseRule4", 110 | Expression = "input1.TrueValue == true" 111 | } 112 | 113 | } 114 | }, 115 | new Rule { 116 | RuleName = "OrRuleFalseTrue", 117 | Operator = "Or", 118 | Rules = new Rule[] { 119 | new Rule{ 120 | RuleName = "trueRule3", 121 | Expression = "input1.TrueValue == false", 122 | }, 123 | new Rule { 124 | RuleName = "falseRule4", 125 | Expression = "input1.TrueValue == true" 126 | } 127 | 128 | } 129 | } 130 | } 131 | }, 132 | new Workflow { 133 | WorkflowName = "EmptyNestedRulesActionsTest", 134 | Rules = new Rule[] { 135 | new Rule { 136 | RuleName = "AndRuleTrueFalse", 137 | Operator = "And", 138 | Rules = new Rule[] { 139 | 140 | }, 141 | Actions = new RuleActions { 142 | OnFailure = new ActionInfo{ 143 | Name = "OutputExpression", 144 | Context = new Dictionary { 145 | { "Expression", "input1.TrueValue" } 146 | } 147 | } 148 | } 149 | } 150 | } 151 | } 152 | 153 | }; 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/ExpressionUtilsTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.HelperFunctions; 5 | using System.Diagnostics.CodeAnalysis; 6 | using Xunit; 7 | 8 | namespace RulesEngine.UnitTest 9 | { 10 | [Trait("Category", "Unit")] 11 | [ExcludeFromCodeCoverage] 12 | public class ExpressionUtilsTest 13 | { 14 | [Fact] 15 | public void CheckContainsTest() 16 | { 17 | var result = ExpressionUtils.CheckContains("", ""); 18 | Assert.False(result); 19 | 20 | result = ExpressionUtils.CheckContains(null, ""); 21 | Assert.False(result); 22 | 23 | result = ExpressionUtils.CheckContains("4", "1,2,3,4,5"); 24 | Assert.True(result); 25 | 26 | result = ExpressionUtils.CheckContains("6", "1,2,3,4,5"); 27 | Assert.False(result); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/LambdaExpressionBuilderTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.ExpressionBuilders; 5 | using RulesEngine.Models; 6 | using System.Collections.Generic; 7 | using System.Diagnostics.CodeAnalysis; 8 | using System.Linq; 9 | using Xunit; 10 | 11 | namespace RulesEngine.UnitTest 12 | { 13 | [Trait("Category", "Unit")] 14 | [ExcludeFromCodeCoverage] 15 | public class LambdaExpressionBuilderTest 16 | { 17 | [Fact] 18 | public void BuildExpressionForRuleTest() 19 | { 20 | var reSettings = new ReSettings(); 21 | var objBuilderFactory = new RuleExpressionBuilderFactory(reSettings, new RuleExpressionParser(reSettings)); 22 | var builder = objBuilderFactory.RuleGetExpressionBuilder(RuleExpressionType.LambdaExpression); 23 | 24 | var ruleParameters = new RuleParameter[] { 25 | new RuleParameter("RequestType","Sales"), 26 | new RuleParameter("RequestStatus", "Active"), 27 | new RuleParameter("RegistrationStatus", "InProcess") 28 | }; 29 | 30 | 31 | var mainRule = new Rule { 32 | RuleName = "rule1", 33 | Operator = "And", 34 | Rules = new List() 35 | }; 36 | 37 | var dummyRule = new Rule { 38 | RuleName = "testRule1", 39 | RuleExpressionType = RuleExpressionType.LambdaExpression, 40 | Expression = "RequestType == \"vod\"" 41 | }; 42 | 43 | mainRule.Rules = mainRule.Rules.Append(dummyRule); 44 | var func = builder.BuildDelegateForRule(dummyRule, ruleParameters); 45 | 46 | Assert.NotNull(func); 47 | Assert.Equal(typeof(RuleResultTree), func.Method.ReturnType); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/ListofRuleResultTreeExtensionTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.Extensions; 5 | using RulesEngine.Models; 6 | using System.Collections.Generic; 7 | using System.Diagnostics.CodeAnalysis; 8 | using Xunit; 9 | 10 | namespace RulesEngine.UnitTest 11 | { 12 | [Trait("Category", "Unit")] 13 | [ExcludeFromCodeCoverage] 14 | public class ListofRuleResultTreeExtensionTest 15 | { 16 | [Fact] 17 | public void OnSuccessWithSuccessTest() 18 | { 19 | var rulesResultTree = new List() 20 | { 21 | new RuleResultTree() 22 | { 23 | ChildResults = null, 24 | ExceptionMessage = string.Empty, 25 | Inputs = new Dictionary(), 26 | IsSuccess = true, 27 | Rule = new Rule() 28 | { 29 | RuleName = "Test Rule 1" 30 | } 31 | }, 32 | new RuleResultTree() 33 | { 34 | ChildResults = null, 35 | ExceptionMessage = string.Empty, 36 | Inputs = new Dictionary(), 37 | IsSuccess = false, 38 | Rule = new Rule() 39 | { 40 | RuleName = "Test Rule 2" 41 | } 42 | }, 43 | 44 | }; 45 | 46 | var successEventName = string.Empty; 47 | 48 | rulesResultTree.OnSuccess((eventName) => { 49 | successEventName = eventName; 50 | }); 51 | 52 | Assert.Equal("Test Rule 1", successEventName); 53 | } 54 | 55 | [Fact] 56 | public void OnSuccessWithSuccessWithEventTest() 57 | { 58 | var rulesResultTree = new List() 59 | { 60 | new RuleResultTree() 61 | { 62 | ChildResults = null, 63 | ExceptionMessage = string.Empty, 64 | Inputs = new Dictionary(), 65 | IsSuccess = true, 66 | Rule = new Rule() 67 | { 68 | RuleName = "Test Rule 1", 69 | SuccessEvent = "Event 1" 70 | } 71 | }, 72 | new RuleResultTree() 73 | { 74 | ChildResults = null, 75 | ExceptionMessage = string.Empty, 76 | Inputs = new Dictionary(), 77 | IsSuccess = false, 78 | Rule = new Rule() 79 | { 80 | RuleName = "Test Rule 2" 81 | } 82 | }, 83 | 84 | }; 85 | 86 | var successEventName = string.Empty; 87 | 88 | rulesResultTree.OnSuccess((eventName) => { 89 | successEventName = eventName; 90 | }); 91 | 92 | Assert.Equal("Event 1", successEventName); 93 | } 94 | 95 | [Fact] 96 | public void OnSuccessWithouSuccessTest() 97 | { 98 | var rulesResultTree = new List() 99 | { 100 | new RuleResultTree() 101 | { 102 | ChildResults = null, 103 | ExceptionMessage = string.Empty, 104 | Inputs = new Dictionary(), 105 | IsSuccess = false, 106 | Rule = new Rule() 107 | { 108 | RuleName = "Test Rule 1" 109 | } 110 | }, 111 | new RuleResultTree() 112 | { 113 | ChildResults = null, 114 | ExceptionMessage = string.Empty, 115 | Inputs = new Dictionary(), 116 | IsSuccess = false, 117 | Rule = new Rule() 118 | { 119 | RuleName = "Test Rule 2" 120 | } 121 | }, 122 | 123 | }; 124 | 125 | var successEventName = string.Empty; 126 | 127 | rulesResultTree.OnSuccess((eventName) => { 128 | successEventName = eventName; 129 | }); 130 | 131 | Assert.Equal(successEventName, string.Empty); 132 | } 133 | 134 | 135 | [Fact] 136 | public void OnFailWithSuccessTest() 137 | { 138 | var rulesResultTree = new List() 139 | { 140 | new RuleResultTree() 141 | { 142 | ChildResults = null, 143 | ExceptionMessage = string.Empty, 144 | Inputs = new Dictionary(), 145 | IsSuccess = true, 146 | Rule = new Rule() 147 | { 148 | RuleName = "Test Rule 1" 149 | } 150 | }, 151 | new RuleResultTree() 152 | { 153 | ChildResults = null, 154 | ExceptionMessage = string.Empty, 155 | Inputs = new Dictionary(), 156 | IsSuccess = false, 157 | Rule = new Rule() 158 | { 159 | RuleName = "Test Rule 2" 160 | } 161 | }, 162 | 163 | }; 164 | 165 | var successEventName = true; 166 | 167 | rulesResultTree.OnFail(() => { 168 | successEventName = false; 169 | }); 170 | 171 | Assert.True(successEventName); 172 | } 173 | 174 | [Fact] 175 | public void OnFailWithoutSuccessTest() 176 | { 177 | var rulesResultTree = new List() 178 | { 179 | new RuleResultTree() 180 | { 181 | ChildResults = null, 182 | ExceptionMessage = string.Empty, 183 | Inputs = new Dictionary(), 184 | IsSuccess = false, 185 | Rule = new Rule() 186 | { 187 | RuleName = "Test Rule 1" 188 | } 189 | }, 190 | new RuleResultTree() 191 | { 192 | ChildResults = null, 193 | ExceptionMessage = string.Empty, 194 | Inputs = new Dictionary(), 195 | IsSuccess = false, 196 | Rule = new Rule() 197 | { 198 | RuleName = "Test Rule 2" 199 | } 200 | }, 201 | 202 | }; 203 | 204 | var successEventName = true; 205 | 206 | rulesResultTree.OnFail(() => { 207 | successEventName = false; 208 | }); 209 | 210 | Assert.False(successEventName); 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/ParameterNameChangeTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.Models; 5 | using System; 6 | using System.Diagnostics.CodeAnalysis; 7 | using System.Dynamic; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | using Xunit; 11 | 12 | namespace RulesEngine.UnitTest 13 | { 14 | [ExcludeFromCodeCoverage] 15 | public class ParameterNameChangeTest 16 | { 17 | [Fact] 18 | public async Task RunTwiceTest_ReturnsExpectedResults() 19 | { 20 | var workflow = new Workflow { 21 | WorkflowName = "ParameterNameChangeWorkflow", 22 | Rules = new Rule[] { 23 | new Rule { 24 | RuleName = "ParameterNameChangeRule", 25 | RuleExpressionType = RuleExpressionType.LambdaExpression, 26 | Expression = "test.blah == 1" 27 | } 28 | } 29 | }; 30 | var engine = new RulesEngine(); 31 | engine.AddOrUpdateWorkflow(workflow); 32 | 33 | dynamic dynamicBlah = new ExpandoObject(); 34 | dynamicBlah.blah = (Int64)1; 35 | var input_pass = new RuleParameter("test", dynamicBlah); 36 | var input_fail = new RuleParameter("SOME_OTHER_NAME", dynamicBlah); 37 | // RuleParameter name matches expression, so should pass. 38 | var pass_results = await engine.ExecuteAllRulesAsync("ParameterNameChangeWorkflow", input_pass); 39 | // RuleParameter name DOES NOT MATCH expression, so should fail. 40 | var fail_results = await engine.ExecuteAllRulesAsync("ParameterNameChangeWorkflow", input_fail); 41 | Assert.True(pass_results.First().IsSuccess); 42 | Assert.False(fail_results.First().IsSuccess); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/RuleCompilerTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.ExpressionBuilders; 5 | using RulesEngine.Models; 6 | using System; 7 | using System.Diagnostics.CodeAnalysis; 8 | using Xunit; 9 | 10 | namespace RulesEngine.UnitTest 11 | { 12 | [Trait("Category", "Unit")] 13 | [ExcludeFromCodeCoverage] 14 | public class RuleCompilerTest 15 | { 16 | [Fact] 17 | public void RuleCompiler_NullCheck() 18 | { 19 | Assert.Throws(() => new RuleCompiler(null, null)); 20 | var reSettings = new ReSettings(); 21 | var parser = new RuleExpressionParser(reSettings); 22 | Assert.Throws(() => new RuleCompiler(null, null)); 23 | } 24 | 25 | [Fact] 26 | public void RuleCompiler_CompileRule_ThrowsException() 27 | { 28 | var reSettings = new ReSettings(); 29 | var parser = new RuleExpressionParser(reSettings); 30 | var compiler = new RuleCompiler(new RuleExpressionBuilderFactory(reSettings, parser),null); 31 | Assert.Throws(() => compiler.CompileRule(null, RuleExpressionType.LambdaExpression,null,null)); 32 | Assert.Throws(() => compiler.CompileRule(null, RuleExpressionType.LambdaExpression, new RuleParameter[] { null },null)); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/RuleExpressionBuilderFactoryTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using RulesEngine.ExpressionBuilders; 5 | using RulesEngine.Models; 6 | using System; 7 | using System.Diagnostics.CodeAnalysis; 8 | using Xunit; 9 | 10 | namespace RulesEngine.UnitTest 11 | { 12 | [Trait("Category", "Unit")] 13 | [ExcludeFromCodeCoverage] 14 | public class RuleExpressionBuilderFactoryTest 15 | { 16 | [Theory] 17 | [InlineData(RuleExpressionType.LambdaExpression, typeof(LambdaExpressionBuilder))] 18 | public void RuleGetExpressionBuilderTest(RuleExpressionType expressionType, Type expectedExpressionBuilderType) 19 | { 20 | var reSettings = new ReSettings(); 21 | var parser = new RuleExpressionParser(reSettings); 22 | var objBuilderFactory = new RuleExpressionBuilderFactory(reSettings, parser); 23 | var builder = objBuilderFactory.RuleGetExpressionBuilder(expressionType); 24 | 25 | var builderType = builder.GetType(); 26 | Assert.Equal(expectedExpressionBuilderType.ToString(), builderType.ToString()); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/RuleExpressionParserTests/RuleExpressionParserTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Newtonsoft.Json.Linq; 5 | using RulesEngine.ExpressionBuilders; 6 | using RulesEngine.Models; 7 | using System.Diagnostics.CodeAnalysis; 8 | using Xunit; 9 | 10 | namespace RulesEngine.UnitTest.RuleExpressionParserTests 11 | { 12 | [ExcludeFromCodeCoverage] 13 | public class RuleExpressionParserTests 14 | { 15 | public RuleExpressionParserTests() { 16 | 17 | 18 | } 19 | 20 | 21 | [Fact] 22 | public void TestExpressionWithJObject() 23 | { 24 | var settings = new ReSettings { 25 | CustomTypes = new[] 26 | { 27 | typeof(JObject), 28 | typeof(JToken), 29 | typeof(JArray) 30 | } 31 | }; 32 | var parser = new RuleExpressionParser(settings); 33 | 34 | var json = @"{ 35 | ""list"": [ 36 | { ""item1"": ""hello"", ""item3"": 1 }, 37 | { ""item2"": ""world"" } 38 | ] 39 | }"; 40 | var input = JObject.Parse(json); 41 | 42 | var result1 = parser.Evaluate( 43 | "Convert.ToInt32(input[\"list\"][0][\"item3\"]) == 1", 44 | new[] { new RuleParameter("input", input) } 45 | ); 46 | Assert.True((bool)result1); 47 | 48 | var result2 = parser.Evaluate( 49 | "Convert.ToString(input[\"list\"][1][\"item2\"]) == \"world\"", 50 | new[] { new RuleParameter("input", input) } 51 | ); 52 | Assert.True((bool)result2); 53 | 54 | var result3 = parser.Evaluate( 55 | "string.Concat(" + 56 | "Convert.ToString(input[\"list\"][0][\"item1\"]), " + 57 | "Convert.ToString(input[\"list\"][1][\"item2\"]))", 58 | new[] { new RuleParameter("input", input) } 59 | ); 60 | Assert.Equal("helloworld", result3); 61 | } 62 | 63 | [Theory] 64 | [InlineData(false)] 65 | public void TestExpressionWithDifferentCompilerSettings(bool fastExpressionEnabled){ 66 | var ruleParser = new RuleExpressionParser(new Models.ReSettings() { UseFastExpressionCompiler = fastExpressionEnabled }); 67 | 68 | decimal? d1 = null; 69 | var result = ruleParser.Evaluate("d1 < 20", new[] { Models.RuleParameter.Create("d1", d1) }); 70 | Assert.False(result); 71 | } 72 | } 73 | 74 | 75 | } 76 | -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/RuleParameterTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using AutoFixture; 5 | using RulesEngine.Models; 6 | using System; 7 | using Xunit; 8 | 9 | namespace RulesEngine.UnitTest; 10 | public class RuleParameterTests 11 | { 12 | [Fact] 13 | public void Create_SetsPropertiesCorrectly() 14 | { 15 | var fixture = new Fixture(); 16 | var name = fixture.Create(); 17 | var type = fixture.Create(); 18 | 19 | var result = RuleParameter.Create(name, type); 20 | 21 | Assert.Equal(name, result.Name); 22 | Assert.Equal(type, result.Type); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/RuleTestClass.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Newtonsoft.Json; 5 | using System.Diagnostics.CodeAnalysis; 6 | 7 | namespace RulesEngine.UnitTest 8 | { 9 | [ExcludeFromCodeCoverage] 10 | public class RuleTestClass 11 | { 12 | [JsonProperty("country")] 13 | public string Country { get; set; } 14 | 15 | [JsonProperty("loyaltyFactor")] 16 | public int loyaltyFactor { get; set; } 17 | public int TotalPurchasesToDate { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/RuleValidationTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Newtonsoft.Json; 5 | using RulesEngine.HelperFunctions; 6 | using RulesEngine.Models; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Diagnostics.CodeAnalysis; 10 | using System.Dynamic; 11 | using System.Linq; 12 | using System.Text.Json.Serialization; 13 | using System.Threading.Tasks; 14 | using Xunit; 15 | 16 | namespace RulesEngine.UnitTest 17 | { 18 | [ExcludeFromCodeCoverage] 19 | public class RuleValidationTest 20 | { 21 | [Fact] 22 | public async Task NullExpressionithLambdaExpression_ReturnsExepectedResults() 23 | { 24 | var workflow = GetNullExpressionithLambdaExpressionWorkflow(); 25 | var reSettings = new ReSettings { }; 26 | RulesEngine rulesEngine = new RulesEngine(); 27 | 28 | Func action = () => { 29 | new RulesEngine(workflow, reSettings: reSettings); 30 | return Task.CompletedTask; 31 | }; 32 | 33 | Exception ex = await Assert.ThrowsAsync(action); 34 | 35 | Assert.Contains(Constants.LAMBDA_EXPRESSION_EXPRESSION_NULL_ERRMSG, ex.Message); 36 | 37 | } 38 | 39 | [Fact] 40 | public async Task NestedRulesWithMissingOperator_ReturnsExepectedResults() 41 | { 42 | var workflow = GetEmptyOperatorWorkflow(); 43 | var reSettings = new ReSettings { }; 44 | RulesEngine rulesEngine = new RulesEngine(); 45 | 46 | Func action = () => { 47 | new RulesEngine(workflow, reSettings: reSettings); 48 | return Task.CompletedTask; 49 | }; 50 | 51 | Exception ex = await Assert.ThrowsAsync(action); 52 | 53 | Assert.Contains(Constants.OPERATOR_RULES_ERRMSG, ex.Message); 54 | 55 | } 56 | 57 | private Workflow[] GetNullExpressionithLambdaExpressionWorkflow() 58 | { 59 | return new[] { 60 | new Workflow { 61 | WorkflowName = "NestedRulesTest", 62 | Rules = new Rule[] { 63 | new Rule { 64 | RuleName = "TestRule", 65 | RuleExpressionType = RuleExpressionType.LambdaExpression, 66 | } 67 | } 68 | } 69 | }; 70 | } 71 | 72 | private Workflow[] GetEmptyOperatorWorkflow() 73 | { 74 | return new[] { 75 | new Workflow { 76 | WorkflowName = "NestedRulesTest", 77 | Rules = new Rule[] { 78 | new Rule { 79 | RuleName = "AndRuleTrueFalse", 80 | Expression = "true == true", 81 | Rules = new Rule[] { 82 | new Rule{ 83 | RuleName = "trueRule1", 84 | Expression = "input1.TrueValue == true", 85 | }, 86 | new Rule { 87 | RuleName = "falseRule1", 88 | Expression = "input1.TrueValue == false" 89 | } 90 | 91 | } 92 | } 93 | } 94 | } 95 | }; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0;net8.0;net9.0 4 | True 5 | ..\..\signing\RulesEngine-publicKey.snk 6 | True 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | PreserveNewest 29 | 30 | 31 | Always 32 | 33 | 34 | PreserveNewest 35 | 36 | 37 | PreserveNewest 38 | 39 | 40 | PreserveNewest 41 | 42 | 43 | PreserveNewest 44 | 45 | 46 | PreserveNewest 47 | 48 | 49 | PreserveNewest 50 | 51 | 52 | PreserveNewest 53 | 54 | 55 | PreserveNewest 56 | 57 | 58 | PreserveNewest 59 | 60 | 61 | -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/TestData/rules1.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkflowName": "inputWorkflow", 3 | "Rules": [ 4 | { 5 | "RuleName": "GiveDiscount10", 6 | "SuccessEvent": "10", 7 | "ErrorMessage": "One or more adjust rules failed.", 8 | "ErrorType": "Error", 9 | "RuleExpressionType": "LambdaExpression", 10 | "Expression": "input1.country == \"canada\" AND input1.loyaltyFactor <= 4" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/TestData/rules10.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkflowName": "inputWorkflow", 3 | "Rules": [ 4 | { 5 | "RuleName": "GiveDiscount10", 6 | "SuccessEvent": "10", 7 | "RuleExpressionType": "LambdaExpression", 8 | "Expression": "input1.Data.GetProperty(\"category\").GetString() == \"abc\"" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/TestData/rules11.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkflowName": "MyWorkflow", 3 | "WorkflowsToInject": null, 4 | "RuleExpressionType": 0, 5 | "GlobalParams": [ 6 | { 7 | "Name": "threshold", 8 | "Expression": "double.Parse(\u00220.25\u0022)" 9 | } 10 | ], 11 | "Rules": [ 12 | { 13 | "RuleName": "Activation", 14 | "Properties": null, 15 | "Operator": null, 16 | "ErrorMessage": null, 17 | "Enabled": true, 18 | "RuleExpressionType": 0, 19 | "WorkflowsToInject": null, 20 | "Rules": null, 21 | "LocalParams": [ 22 | { 23 | "Name": "ruleCount", 24 | "Expression": "int.Parse(\u002215\u0022)" 25 | } 26 | ], 27 | "Expression": "input1.Count \u003E= ruleCount \u0026\u0026 input1.Where(x =\u003E x.Value \u003E= threshold).Count() \u003E= ruleCount", 28 | "Actions": null, 29 | "SuccessEvent": null 30 | }, 31 | { 32 | "RuleName": "Deactivation", 33 | "Properties": null, 34 | "Operator": null, 35 | "ErrorMessage": null, 36 | "Enabled": true, 37 | "RuleExpressionType": 0, 38 | "WorkflowsToInject": null, 39 | "Rules": null, 40 | "LocalParams": [ 41 | { 42 | "Name": "ruleCount", 43 | "Expression": "int.Parse(\u002230\u0022)" 44 | } 45 | ], 46 | "Expression": "input1.Count \u003E= ruleCount \u0026\u0026 input1.OrderByDescending(o =\u003E o.ChangeDateTime).Take(ruleCount).All(a =\u003E a.Value \u003C threshold)", 47 | "Actions": null, 48 | "SuccessEvent": null 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/TestData/rules2.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkflowName": "inputWorkflow", 3 | "Rules": [ 4 | { 5 | "RuleName": "Rule1", 6 | "Operator": "Or", 7 | "ErrorMessage": "One or more adjust rules failed.", 8 | "ErrorType": "Error", 9 | "Rules": [ 10 | { 11 | "RuleName": "GiveDiscount10", 12 | "SuccessEvent": "10", 13 | "ErrorMessage": "One or more adjust rules failed.", 14 | "ErrorType": "Error", 15 | "RuleExpressionType": "LambdaExpression", 16 | "Expression": "input1.country == \"india\" AND input1.loyaltyFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2" 17 | }, 18 | { 19 | "RuleName": "GiveDiscount20", 20 | "SuccessEvent": "20", 21 | "ErrorMessage": "One or more adjust rules failed.", 22 | "ErrorType": "Error", 23 | "RuleExpressionType": "LambdaExpression", 24 | "Expression": "input1.country == \"india\" AND input1.loyaltyFactor == 3 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2" 25 | }, 26 | { 27 | "RuleName": "GiveDiscount25", 28 | "SuccessEvent": "25", 29 | "ErrorMessage": "One or more adjust rules failed.", 30 | "ErrorType": "Error", 31 | "RuleExpressionType": "LambdaExpression", 32 | "Expression": "input1.country != \"india\" AND input1.loyaltyFactor >= 2 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 5" 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/TestData/rules3.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkflowName": "inputWorkflow", 3 | "Rules": [ 4 | { 5 | "RuleName": "GiveDiscount10", 6 | "SuccessEvent": "10", 7 | "ErrorMessage": "One or more adjust rules failed.", 8 | "ErrorType": "Error", 9 | "RuleExpressionType": "LambdaExpression", 10 | "Expression": "input1.couy == \"india\" AND input1.loyaltyFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input2.noOfVisitsPerMonth > 2" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/TestData/rules4.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "WorkflowName": "inputWorkflow", 4 | "Rules": [ 5 | { 6 | "RuleName": "GiveDiscount10", 7 | "SuccessEvent": "10", 8 | "ErrorMessage": "One or more adjust rules failed, with loyaltyFactor : $(model1.loyaltyFactor), country : $(model1.country), totalPurchasesToDate : $(model1.totalPurchasesToDate), model2 : $(model2)", 9 | "ErrorType": "Error", 10 | "localParams": [ 11 | { 12 | "Name": "model1", 13 | "Expression": "input1.FirstOrDefault(country.Equals(\"india\", StringComparison.OrdinalIgnoreCase))" 14 | }, 15 | { 16 | "Name": "model2", 17 | "Expression": "model1.country == \"india\"" 18 | } 19 | ], 20 | "RuleExpressionType": "LambdaExpression", 21 | "Expression": "model1.country == \"india\" AND model1.loyaltyFactor <= 2 AND model1.totalPurchasesToDate >= 5000 AND model2" 22 | }, 23 | { 24 | "RuleName": "GiveDiscount100", 25 | "SuccessEvent": "10", 26 | "ErrorType": "Error", 27 | "localParams": [ 28 | { 29 | "Name": "model1", 30 | "Expression": "input1.FirstOrDefault(country.Equals(\"india\", StringComparison.OrdinalIgnoreCase))" 31 | }, 32 | { 33 | "Name": "model2", 34 | "Expression": "model1.country == \"india\"" 35 | } 36 | ], 37 | "RuleExpressionType": "LambdaExpression", 38 | "Expression": "model1.country == \"india\" AND model1.loyaltyFactor < 0 AND model1.totalPurchasesToDate >= 5000 AND model2" 39 | }, 40 | { 41 | "RuleName": "GiveDiscount25", 42 | "SuccessEvent": "25", 43 | "ErrorMessage": "One or more adjust rules failed, country : $(input4.country), loyaltyFactor : $(input4.loyaltyFactor), totalPurchasesToDate : $(input4.totalPurchasesToDate), totalOrders : $(input5.totalOrders), noOfVisitsPerMonth : $(input30.noOfVisitsPerMonth), $(model2)", 44 | "ErrorType": "Error", 45 | "localParams": [ 46 | { 47 | "Name": "model1", 48 | "Expression": "input1.FirstOrDefault(country.Equals(\"india\", StringComparison.OrdinalIgnoreCase))" 49 | }, 50 | { 51 | "Name": "model2", 52 | "Expression": "model1.country == \"india\"" 53 | } 54 | ], 55 | "RuleExpressionType": "LambdaExpression", 56 | "Expression": "input4.country == \"india\" AND input4.loyaltyFactor >= 2 AND input4.totalPurchasesToDate <= 10 AND input5.totalOrders > 2 AND input3.noOfVisitsPerMonth > 5" 57 | }, 58 | { 59 | "RuleName": "GiveDiscount30", 60 | "SuccessEvent": "30", 61 | "ErrorMessage": "One or more adjust rules failed.", 62 | "ErrorType": "Error", 63 | "RuleExpressionType": "LambdaExpression", 64 | "Expression": "input4.loyaltyFactor > 30 AND input4.totalPurchasesToDate >= 50000 AND input4.totalPurchasesToDate <= 100000 AND input5.totalOrders > 5 AND input3.noOfVisitsPerMonth > 15" 65 | }, 66 | { 67 | "RuleName": "GiveDiscount35", 68 | "SuccessEvent": "35", 69 | "ErrorMessage": "One or more adjust rules failed, totalPurchasesToDate : $(input4.totalPurchasesToDate), totalOrders : $(input5.totalOrders)", 70 | "ErrorType": "Error", 71 | "RuleExpressionType": "LambdaExpression", 72 | "Expression": "input4.loyaltyFactor > 30 AND input4.totalPurchasesToDate >= 100000 AND input5.totalOrders > 15 AND input3.noOfVisitsPerMonth > 25" 73 | } 74 | ] 75 | } 76 | ] -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/TestData/rules5.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkflowName": "inputWorkflow", 3 | "Rules": [ 4 | { 5 | "RuleName": "upperCaseAccess", 6 | "SuccessEvent": "10", 7 | "ErrorMessage": "One or more adjust rules failed.", 8 | "ErrorType": "Error", 9 | "RuleExpressionType": "LambdaExpression", 10 | "Expression": "utils.CheckExists(String(input1.Property1)) == true" 11 | }, 12 | { 13 | "RuleName": "lowerCaseAccess", 14 | "SuccessEvent": "10", 15 | "ErrorMessage": "One or more adjust rules failed.", 16 | "ErrorType": "Error", 17 | "RuleExpressionType": "LambdaExpression", 18 | "Expression": "utils.CheckExists(String(input1.property1)) == true" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/TestData/rules6.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkflowName": "inputWorkflow", 3 | "Rules": [ 4 | { 5 | "RuleName": "GiveDiscount10", 6 | "SuccessEvent": "10", 7 | "ErrorMessage": "One or more adjust rules failed.", 8 | "ErrorType": "Error", 9 | "RuleExpressionType": "LambdaExpression", 10 | "Expression": "input1.Property1.Contains(\"hell\")" 11 | }, 12 | { 13 | "RuleName": "GiveDiscount20", 14 | "SuccessEvent": "20", 15 | "ErrorMessage": "One or more adjust rules failed.", 16 | "ErrorType": "Error", 17 | "RuleExpressionType": "LambdaExpression", 18 | "Expression": "input1.Property1.Contains(\"hell\") && !input1.Boolean" 19 | }, 20 | { 21 | "RuleName": "GiveDiscount30", 22 | "SuccessEvent": "30", 23 | "ErrorMessage": "One or more adjust rules failed.", 24 | "ErrorType": "Error", 25 | "RuleExpressionType": "LambdaExpression", 26 | "Expression": "input1.Method.Invoke()" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/TestData/rules7.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkflowName": "inputWorkflow", 3 | "Rules": [ 4 | { 5 | "RuleName": "GiveDiscount10", 6 | "SuccessEvent": "10", 7 | "ErrorMessage": "One or more adjust rules failed.", 8 | "ErrorType": "Error", 9 | "RuleExpressionType": "LambdaExpression", 10 | "Expression": "!input1.Boolean" 11 | }, 12 | { 13 | "RuleName": "GiveDiscount20", 14 | "SuccessEvent": "20", 15 | "ErrorMessage": "One or more adjust rules failed.", 16 | "ErrorType": "Error", 17 | "RuleExpressionType": "LambdaExpression", 18 | "Expression": "!input1.Boolean && true" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/TestData/rules8.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkflowName": "inputWorkflow", 3 | "Rules": [ 4 | { 5 | "RuleName": "GiveDiscount10", 6 | "SuccessEvent": "10", 7 | "ErrorMessage": "One or more adjust rules failed.", 8 | "ErrorType": "Error", 9 | "RuleExpressionType": "LambdaExpression", 10 | "Expression": "input1.Boolean" 11 | }, 12 | { 13 | "RuleName": "GiveDiscount20", 14 | "SuccessEvent": "20", 15 | "ErrorMessage": "One or more adjust rules failed.", 16 | "ErrorType": "Error", 17 | "RuleExpressionType": "LambdaExpression", 18 | "Expression": "input1.Boolean && true || (input1.Boolean)" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/TestData/rules9.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkflowName": "inputWorkflow", 3 | "Rules": [ 4 | { 5 | "RuleName": "GiveDiscount10", 6 | "SuccessEvent": "10", 7 | "ErrorMessage": "One or more adjust rules failed.", 8 | "ErrorType": "Error", 9 | "RuleExpressionType": "LambdaExpression", 10 | "Expression": "input1.Boolean" 11 | }, 12 | { 13 | "RuleName": "GiveDiscount20", 14 | "SuccessEvent": "20", 15 | "ErrorMessage": "One or more adjust rules failed.", 16 | "ErrorType": "Error", 17 | "RuleExpressionType": "LambdaExpression", 18 | "Expression": "input1.Boolean && input1.Data.NotExistingMethod()" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /test/RulesEngine.UnitTest/UtilsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | using Newtonsoft.Json.Linq; 5 | using RulesEngine.HelperFunctions; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Diagnostics.CodeAnalysis; 9 | using System.Dynamic; 10 | using Xunit; 11 | 12 | namespace RulesEngine.UnitTest 13 | { 14 | [ExcludeFromCodeCoverage] 15 | public class TestClass 16 | { 17 | public string Test { get; set; } 18 | public List TestList { get; set; } 19 | } 20 | 21 | [Trait("Category", "Unit")] 22 | [ExcludeFromCodeCoverage] 23 | public class UtilsTests 24 | { 25 | 26 | [Fact] 27 | public void GetTypedObject_dynamicObject() 28 | { 29 | dynamic obj = new ExpandoObject(); 30 | obj.Test = "hello"; 31 | obj.TestList = new List { 1, 2, 3 }; 32 | object typedobj = Utils.GetTypedObject(obj); 33 | Assert.IsNotType(typedobj); 34 | Assert.NotNull(typedobj.GetType().GetProperty("Test")); 35 | } 36 | 37 | [Fact] 38 | public void GetTypedObject_dynamicObject_multipleObjects() 39 | { 40 | dynamic obj = new ExpandoObject(); 41 | obj.Test = "hello"; 42 | obj.TestList = new List { 1, 2, 3 }; 43 | dynamic obj2 = new ExpandoObject(); 44 | obj2.Test = "world"; 45 | obj2.TestList = new List { 1, 2, 3 }; 46 | object typedobj = Utils.GetTypedObject(obj); 47 | object typedobj2 = Utils.GetTypedObject(obj2); 48 | Assert.IsNotType(typedobj); 49 | Assert.NotNull(typedobj.GetType().GetProperty("Test")); 50 | Assert.Equal(typedobj.GetType(), typedobj2.GetType()); 51 | } 52 | 53 | 54 | [Fact] 55 | public void GetTypedObject_nonDynamicObject() 56 | { 57 | var obj = new { 58 | Test = "hello" 59 | }; 60 | var typedobj = Utils.GetTypedObject(obj); 61 | Assert.IsNotType(typedobj); 62 | Assert.NotNull(typedobj.GetType().GetProperty("Test")); 63 | } 64 | 65 | 66 | [Fact] 67 | public void GetJObject_nonDynamicObject() 68 | { 69 | dynamic obj = JObject.FromObject(new { 70 | Test = "hello" 71 | }); 72 | dynamic typedobj = Utils.GetTypedObject(obj); 73 | Assert.IsNotType(typedobj); 74 | Assert.IsType(typedobj); 75 | Assert.NotNull(typedobj.Test); 76 | } 77 | 78 | 79 | [Fact] 80 | public void CreateObject_dynamicObject() 81 | { 82 | dynamic obj = new ExpandoObject(); 83 | obj.Test = "test"; 84 | obj.TestList = new List { 1, 2, 3 }; 85 | 86 | object newObj = Utils.CreateObject(typeof(TestClass), obj); 87 | Assert.IsNotType(newObj); 88 | Assert.NotNull(newObj.GetType().GetProperty("Test")); 89 | 90 | } 91 | 92 | [Fact] 93 | public void CreateAbstractType_dynamicObject() 94 | { 95 | dynamic obj = new ExpandoObject(); 96 | obj.Test = "test"; 97 | obj.TestList = new List { 1, 2, 3 }; 98 | obj.testEmptyList = new List(); 99 | 100 | Type type = Utils.CreateAbstractClassType(obj); 101 | Assert.NotEqual(typeof(ExpandoObject), type); 102 | Assert.NotNull(type.GetProperty("Test")); 103 | 104 | } 105 | 106 | 107 | } 108 | } 109 | --------------------------------------------------------------------------------