commit b960802babedf3b9cc38cc71aed1fd11e95d6c22 Author: Dishant Munjal Date: Tue Aug 13 15:36:57 2019 +0530 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..31eecfc --- /dev/null +++ b/.gitignore @@ -0,0 +1,331 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ +/src/RulesEngine/RulesEngine.sln.licenseheader diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f9ba8cf --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,9 @@ +# Microsoft Open Source Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +Resources: + +- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) +- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9e841e7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/README.md b/README.md new file mode 100644 index 0000000..322b099 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# Rules Engine + +## Overview +Rules Engine is a NuGet Package for abstracting business logic/rules/policies out of the system. This works in a very simple way by giving you an ability to put your rules in a store outside the core logic of the system thus ensuring that any change in rules doesn't affect the core system. + +## Installation +To install this NuGet Package, please install the NuGet RulesEngine from [NuGet.org](https://www.nuget.org/) using manage nuget explorer in visual studio. + +## How to use it + +To use the Rules Engine, please install it as a NuGet Package into the project you want to use. + +You need to store the rules based on the [schema definition](https://github.com/microsoft/RulesEngine/blob/master/schema/workflowRules-schema.json) given and they can be stored in any store as deemed appropriate like Azure Blob Storage, Cosmos DB, Azure App Configuration, SQL Servers, file systems etc. The expressions are supposed to be a [lambda expressions](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions). + +An example rule could be - +```json +[ + { + "WorkflowName": "Discount", + "Rules": [ + { + "RuleName": "GiveDiscount10", + "SuccessEvent": "10", + "ErrorMessage": "One or more adjust rules failed.", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression", + "Expression": "input1.country == \"india\" AND input1.loyalityFactor <= 2 AND input1.totalPurchasesToDate >= 5000" + }, + { + "RuleName": "GiveDiscount20", + "SuccessEvent": "20", + "ErrorMessage": "One or more adjust rules failed.", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression", + "Expression": "input1.country == \"india\" AND input1.loyalityFactor >= 3 AND input1.totalPurchasesToDate >= 10000" + } + ] + } +] +``` + +You can inject the rules into the Rules Engine by initiating an instance by using the following code - +```c# +var rulesEngine = new RulesEngine(workflowRules, logger); +``` +Here, *workflowRules* is a list of deserialized object based out of the schema explained above and *logger* is a custom logger instance made out of an [ILogger](https://github.com/microsoft/RulesEngine/wiki/Getting-Started#logger) instance. + +Once done, the Rules Engine needs to execute the rules for a given input. It can be done by calling the method ExecuteRule as shown below - +```c# +List response = rulesEngine.ExecuteRule(workflowName, input); +``` +Here, *workflowName* is the name of the workflow, which is *Discount* in the above mentioned example. And *input* is the object which needs to be checked against the rules. + +The *response* will contain a list of [*RuleResultTree*](https://github.com/microsoft/RulesEngine/wiki/Getting-Started#ruleresulttree) which gives information if a particular rule passed or failed. + + +_Note: A detailed example showcasing how to use Rules Engine is explained in [Getting Started page](https://github.com/microsoft/RulesEngine/wiki/Getting-Started) of [Rules Engine Wiki](https://github.com/microsoft/RulesEngine/wiki)._ + +_A demo app for the is available at [this location](https://github.com/microsoft/RulesEngine/tree/master/demo)._ + +## How it works + +![](https://github.com/microsoft/RulesEngine/blob/master/assets/BlockDiagram.png) + +The rules can be stored in any store and be fed to the system in a structure which follows a proper [schema](https://github.com/microsoft/RulesEngine/blob/master/schema/workflowRules-schema.json) of WorkFlow model. + +The wrapper needs to be created over the Rules Engine package, which will get the rules and input message(s) from any store that your system dictates and put it into the Engine. Also, the wrapper then needs to handle the output using appropriate means. + + +_Note: To know in detail of the workings of Rules Engine, please visit [How it works section](https://github.com/microsoft/RulesEngine/wiki/Introduction#how-it-works) in [Rules Engine Wiki](https://github.com/microsoft/RulesEngine/wiki)._ + +## Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + + + + +--- + +_For more details please check out [Rules Engine Wiki](https://github.com/microsoft/RulesEngine/wiki)._ diff --git a/assets/BlockDiagram.png b/assets/BlockDiagram.png new file mode 100644 index 0000000..688e84a Binary files /dev/null and b/assets/BlockDiagram.png differ diff --git a/demo/DemoApp/DemoApp.csproj b/demo/DemoApp/DemoApp.csproj new file mode 100644 index 0000000..e1891e5 --- /dev/null +++ b/demo/DemoApp/DemoApp.csproj @@ -0,0 +1,19 @@ + + + + Exe + netcoreapp3.0 + + + + + + + + + + PreserveNewest + + + + diff --git a/demo/DemoApp/Program.cs b/demo/DemoApp/Program.cs new file mode 100644 index 0000000..687e22a --- /dev/null +++ b/demo/DemoApp/Program.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using RulesEngine.Models; +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.IO; +using static RulesEngine.Extensions.ListofRuleResultTreeExtension; + +namespace DemoApp +{ + class Program + { + static void Main(string[] args) + { + var basicInfo = "{\"name\": \"Dishant\",\"email\": \"dishantmunjal@live.com\",\"creditHistory\": \"good\",\"country\": \"canada\",\"loyalityFactor\": 3,\"totalPurchasesToDate\": 10000}"; + var orderInfo = "{\"totalOrders\": 5,\"recurringItems\": 2}"; + var telemetryInfo = "{\"noOfVisitsPerMonth\": 10,\"percentageOfBuyingToVisit\": 15}"; + + var converter = new ExpandoObjectConverter(); + + dynamic input1 = JsonConvert.DeserializeObject(basicInfo, converter); + dynamic input2 = JsonConvert.DeserializeObject(orderInfo, converter); + dynamic input3 = JsonConvert.DeserializeObject(telemetryInfo, converter); + + var inputs = new dynamic[] + { + input1, + input2, + input3 + }; + + var files = Directory.GetFiles(Directory.GetCurrentDirectory(), "Discount.json", SearchOption.AllDirectories); + if (files == null || files.Length == 0) + throw new Exception("Rules not found."); + + var fileData = File.ReadAllText(files[0]); + var workflowRules = JsonConvert.DeserializeObject>(fileData); + + var bre = new RulesEngine.RulesEngine(workflowRules.ToArray(), null); + + string discountOffered = "No discount offered."; + + List resultList = bre.ExecuteRule("Discount", inputs); + + resultList.OnSuccess((eventName) => + { + discountOffered = $"Discount offered is {eventName} % over MRP."; + }); + + resultList.OnFail(() => + { + discountOffered = "The user is not eligible for any discount."; + }); + + Console.WriteLine(discountOffered); + + } + } +} diff --git a/demo/DemoApp/Workflows/Discount.json b/demo/DemoApp/Workflows/Discount.json new file mode 100644 index 0000000..a46f4f1 --- /dev/null +++ b/demo/DemoApp/Workflows/Discount.json @@ -0,0 +1,47 @@ +[ + { + "WorkflowName": "Discount", + "Rules": [ + { + "RuleName": "GiveDiscount10", + "SuccessEvent": "10", + "ErrorMessage": "One or more adjust rules failed.", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression", + "Expression": "input1.country == \"india\" AND input1.loyalityFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2" + }, + { + "RuleName": "GiveDiscount20", + "SuccessEvent": "20", + "ErrorMessage": "One or more adjust rules failed.", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression", + "Expression": "input1.country == \"india\" AND input1.loyalityFactor == 3 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2" + }, + { + "RuleName": "GiveDiscount25", + "SuccessEvent": "25", + "ErrorMessage": "One or more adjust rules failed.", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression", + "Expression": "input1.country != \"india\" AND input1.loyalityFactor >= 2 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 5" + }, + { + "RuleName": "GiveDiscount30", + "SuccessEvent": "30", + "ErrorMessage": "One or more adjust rules failed.", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression", + "Expression": "input1.loyalityFactor > 3 AND input1.totalPurchasesToDate >= 50000 AND input1.totalPurchasesToDate <= 100000 AND input2.totalOrders > 5 AND input3.noOfVisitsPerMonth > 15" + }, + { + "RuleName": "GiveDiscount35", + "SuccessEvent": "35", + "ErrorMessage": "One or more adjust rules failed.", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression", + "Expression": "input1.loyalityFactor > 3 AND input1.totalPurchasesToDate >= 100000 AND input2.totalOrders > 15 AND input3.noOfVisitsPerMonth > 25" + } + ] + } +] \ No newline at end of file diff --git a/schema/workflowRules-schema.json b/schema/workflowRules-schema.json new file mode 100644 index 0000000..f1dd723 --- /dev/null +++ b/schema/workflowRules-schema.json @@ -0,0 +1,87 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Rule": { + "properties": { + "RuleName": { + "type": "string" + }, + "Operator": { + "enum": [ "And", "AndAlso", "Or", "OrElse" ] + }, + "ErrorMessage": { + "type": "string" + }, + "ErrorType": { + "enum": [ "Warning", "Error" ] + }, + "SuccessEvent": { + "type": "string" + }, + "Rules": { + "type": "array", + "items": { + "anyOf": [ + { "$ref": "#/definitions/LeafRule" }, + { "$ref": "#/definitions/Rule" } + ] + } + } + }, + "required": [ + "RuleName", + "Operator", + "Rules" + ], + "type": "object" + }, + "LeafRule": { + "type": "object", + "required": [ + "RuleName", + "Expression", + "RuleExpressionType" + ], + "properties": { + "RuleName": { + "type": "string" + }, + "Expression": { + "type": "string" + }, + "RuleExpressionType": { + "enum": [ "LambdaExpression" ] + }, + "ErrorMessage": { + "type": "string" + }, + "ErrorType": { + "enum": [ "Warning", "Error" ] + }, + "SuccessEvent": { + "type": "string" + } + } + } + }, + "properties": { + "WorkFlowName": { + "type": "string" + }, + "Rules": { + "type": "array", + "items": { + "anyOf": [ + { "$ref": "#/definitions/LeafRule" }, + { "$ref": "#/definitions/Rule" } + ] + } + } + }, + "required": [ + "WorkflowName", + "Rules" + ], + "type": "object" +} + diff --git a/src/RulesEngine/RulesEngine.sln b/src/RulesEngine/RulesEngine.sln new file mode 100644 index 0000000..554b8b2 --- /dev/null +++ b/src/RulesEngine/RulesEngine.sln @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29123.89 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RulesEngine", "RulesEngine\RulesEngine.csproj", "{CD4DFE6A-083B-478E-8377-77F474833E30}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RulesEngine.UnitTest", "..\..\test\RulesEngine.UnitTest\RulesEngine.UnitTest.csproj", "{50E0C2A5-E2C8-4B12-8C0E-B69F698A82BF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DemoApp", "..\..\demo\DemoApp\DemoApp.csproj", "{57BB8C07-799A-4F87-A7CC-D3D3F694DD02}" + ProjectSection(ProjectDependencies) = postProject + {CD4DFE6A-083B-478E-8377-77F474833E30} = {CD4DFE6A-083B-478E-8377-77F474833E30} + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CD4DFE6A-083B-478E-8377-77F474833E30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD4DFE6A-083B-478E-8377-77F474833E30}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD4DFE6A-083B-478E-8377-77F474833E30}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD4DFE6A-083B-478E-8377-77F474833E30}.Release|Any CPU.Build.0 = Release|Any CPU + {50E0C2A5-E2C8-4B12-8C0E-B69F698A82BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50E0C2A5-E2C8-4B12-8C0E-B69F698A82BF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50E0C2A5-E2C8-4B12-8C0E-B69F698A82BF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50E0C2A5-E2C8-4B12-8C0E-B69F698A82BF}.Release|Any CPU.Build.0 = Release|Any CPU + {57BB8C07-799A-4F87-A7CC-D3D3F694DD02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {57BB8C07-799A-4F87-A7CC-D3D3F694DD02}.Debug|Any CPU.Build.0 = Debug|Any CPU + {57BB8C07-799A-4F87-A7CC-D3D3F694DD02}.Release|Any CPU.ActiveCfg = Release|Any CPU + {57BB8C07-799A-4F87-A7CC-D3D3F694DD02}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E1F2EC8E-4005-4DFE-90ED-296D4592867A} + EndGlobalSection +EndGlobal diff --git a/src/RulesEngine/RulesEngine/CustomTypeProvider.cs b/src/RulesEngine/RulesEngine/CustomTypeProvider.cs new file mode 100644 index 0000000..df1359d --- /dev/null +++ b/src/RulesEngine/RulesEngine/CustomTypeProvider.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine.HelperFunctions; +using System; +using System.Collections.Generic; +using System.Linq.Dynamic.Core.CustomTypeProviders; + +namespace RulesEngine +{ + public class CustomTypeProvider : DefaultDynamicLinqCustomTypeProvider + { + private HashSet _types; + public CustomTypeProvider(Type[] types) : base() + { + _types = new HashSet(types ?? new Type[] { }); + _types.Add(typeof(ExpressionUtils)); + } + + public override HashSet GetCustomTypes() + { + return _types; + } + } +} diff --git a/src/RulesEngine/RulesEngine/Exceptions/RuleValidationException.cs b/src/RulesEngine/RulesEngine/Exceptions/RuleValidationException.cs new file mode 100644 index 0000000..a41b554 --- /dev/null +++ b/src/RulesEngine/RulesEngine/Exceptions/RuleValidationException.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using FluentValidation; +using FluentValidation.Results; +using System.Collections.Generic; + +namespace RulesEngine.Exceptions +{ + public class RuleValidationException : ValidationException + { + public RuleValidationException(string message, IEnumerable errors) : base(message, errors) + { + } + } +} diff --git a/src/RulesEngine/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs b/src/RulesEngine/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs new file mode 100644 index 0000000..7ee6df2 --- /dev/null +++ b/src/RulesEngine/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine; +using RulesEngine.ExpressionBuilders; +using RulesEngine.HelperFunctions; +using RulesEngine.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Linq.Expressions; + +namespace RulesEngine.ExpressionBuilders +{ + /// + /// This class will build the list expression + /// + internal sealed class LambdaExpressionBuilder : RuleExpressionBuilderBase + { + private readonly ReSettings _reSettings; + + internal LambdaExpressionBuilder(ReSettings reSettings) + { + _reSettings = reSettings; + } + internal override Expression> BuildExpressionForRule(Rule rule, IEnumerable typeParamExpressions, ParameterExpression ruleInputExp) + { + var config = new ParsingConfig { CustomTypeProvider = new CustomTypeProvider(_reSettings.CustomTypes) }; + var e = DynamicExpressionParser.ParseLambda(config, typeParamExpressions.ToArray(), null, rule.Expression); + var body = (BinaryExpression)e.Body; + return Helpers.ToResultTreeExpression(rule, null, body, typeParamExpressions, ruleInputExp); + } + } +} diff --git a/src/RulesEngine/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs b/src/RulesEngine/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs new file mode 100644 index 0000000..0130138 --- /dev/null +++ b/src/RulesEngine/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine.Models; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace RulesEngine.ExpressionBuilders +{ + /// + /// Base class for expression builders + /// + internal abstract class RuleExpressionBuilderBase + { + /// + /// Builds the expression for rule. + /// + /// The rule. + /// The type parameter expressions. + /// The rule input exp. + /// Expression type + internal abstract Expression> BuildExpressionForRule(Rule rule, IEnumerable typeParamExpressions, ParameterExpression ruleInputExp); + } +} diff --git a/src/RulesEngine/RulesEngine/Extensions/ListofRuleResultTreeExtension.cs b/src/RulesEngine/RulesEngine/Extensions/ListofRuleResultTreeExtension.cs new file mode 100644 index 0000000..38d65f8 --- /dev/null +++ b/src/RulesEngine/RulesEngine/Extensions/ListofRuleResultTreeExtension.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine.Models; +using System.Collections.Generic; +using System.Linq; + + +namespace RulesEngine.Extensions +{ + public static class ListofRuleResultTreeExtension + { + public delegate void OnSuccessFunc(string eventName); + public delegate void OnFailureFunc(); + + public static List OnSuccess(this List ruleResultTrees, OnSuccessFunc onSuccessFunc) + { + var successfulRuleResult = ruleResultTrees.FirstOrDefault(ruleResult => ruleResult.IsSuccess == true); + if (successfulRuleResult != null) + { + var eventName = successfulRuleResult.Rule.SuccessEvent ?? successfulRuleResult.Rule.RuleName; + onSuccessFunc(eventName); + } + + return ruleResultTrees; + } + + public static List OnFail(this List ruleResultTrees, OnFailureFunc onFailureFunc) + { + bool allFailure = ruleResultTrees.All(ruleResult => ruleResult.IsSuccess == false); + if (allFailure) + onFailureFunc(); + return ruleResultTrees; + } + } +} diff --git a/src/RulesEngine/RulesEngine/HelperFunctions/Constants.cs b/src/RulesEngine/RulesEngine/HelperFunctions/Constants.cs new file mode 100644 index 0000000..1c482a8 --- /dev/null +++ b/src/RulesEngine/RulesEngine/HelperFunctions/Constants.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace RulesEngine.HelperFunctions +{ + /// + /// Constants + /// + public static class Constants + { + public const string WORKFLOW_NAME_NULL_ERRMSG = "Workflow name can not be null or empty"; + public const string INJECT_WORKFLOW_RULES_ERRMSG = "Atleast one of Rules or WorkflowRulesToInject must be not empty"; + public const string RULE_CATEGORY_CONFIGURED_ERRMSG = "Rule Category should be configured"; + public const string RULE_NULL_ERRMSG = "Rules can not be null or zero"; + public const string NESTED_RULE_NULL_ERRMSG = "Nested rules can not be null"; + public const string NESTED_RULE_CONFIGURED_ERRMSG = "Nested rules can not be configured"; + public const string OPERATOR_NULL_ERRMSG = "Operator can not be null"; + public const string OPERATOR_INCORRECT_ERRMSG = "Operator {PropertyValue} is not allowed"; + public const string RULE_NAME_NULL_ERRMSG = "Rule Name can not be null"; + public const string LAMBDA_EXPRESSION_EXPRESSION_NULL_ERRMSG = "Expression cannot be null or empty when RuleExpressionType is LambdaExpression"; + public const string LAMBDA_EXPRESSION_OPERATOR_ERRMSG = "Cannot use Operator field when RuleExpressionType is LambdaExpression"; + public const string LAMBDA_EXPRESSION_RULES_ERRMSG = "Cannot use Rules field when RuleExpressionType is LambdaExpression"; + + } +} diff --git a/src/RulesEngine/RulesEngine/HelperFunctions/ExpressionUtils.cs b/src/RulesEngine/RulesEngine/HelperFunctions/ExpressionUtils.cs new file mode 100644 index 0000000..d725cf0 --- /dev/null +++ b/src/RulesEngine/RulesEngine/HelperFunctions/ExpressionUtils.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Linq; + +namespace RulesEngine.HelperFunctions +{ + public static class ExpressionUtils + { + public static bool CheckContains(string check, string valList) + { + if (String.IsNullOrEmpty(check) || String.IsNullOrEmpty(valList)) + return false; + + var list = valList.Split(',').ToList(); + return list.Contains(check); + } + } +} diff --git a/src/RulesEngine/RulesEngine/HelperFunctions/Helpers.cs b/src/RulesEngine/RulesEngine/HelperFunctions/Helpers.cs new file mode 100644 index 0000000..222a27d --- /dev/null +++ b/src/RulesEngine/RulesEngine/HelperFunctions/Helpers.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace RulesEngine.HelperFunctions +{ + /// + /// Helpers + /// + internal static class Helpers + { + /// + /// To the result tree expression. + /// + /// The rule. + /// The child rule results. + /// The is success exp. + /// The type parameter expressions. + /// The rule input exp. + /// Expression of func + internal static Expression> ToResultTreeExpression(Rule rule, IEnumerable childRuleResults, BinaryExpression isSuccessExp, IEnumerable typeParamExpressions, ParameterExpression ruleInputExp) + { + var memberInit = ToResultTree(rule, childRuleResults, isSuccessExp, typeParamExpressions, null); + var lambda = Expression.Lambda>(memberInit, new[] { ruleInputExp }); + return lambda; + } + + /// + /// To the result tree member expression + /// + /// The rule. + /// The child rule results. + /// The is success exp. + /// The child rule results block expression. + /// + internal static MemberInitExpression ToResultTree(Rule rule, IEnumerable childRuleResults, BinaryExpression isSuccessExp, IEnumerable typeParamExpressions, BlockExpression childRuleResultsblockexpr) + { + var createdType = typeof(RuleResultTree); + var ctor = Expression.New(createdType); + + var ruleProp = createdType.GetProperty(nameof(RuleResultTree.Rule)); + var isSuccessProp = createdType.GetProperty(nameof(RuleResultTree.IsSuccess)); + var childResultProp = createdType.GetProperty(nameof(RuleResultTree.ChildResults)); + var inputProp = createdType.GetProperty(nameof(RuleResultTree.Input)); + + var rulePropBinding = Expression.Bind(ruleProp, Expression.Constant(rule)); + var isSuccessPropBinding = Expression.Bind(isSuccessProp, isSuccessExp); + var inputBinding = Expression.Bind(inputProp, typeParamExpressions.FirstOrDefault()); + + MemberInitExpression memberInit; + + if (childRuleResults != null) + { + var ruleResultTreeArr = Expression.NewArrayInit(typeof(RuleResultTree), childRuleResults); + + var childResultPropBinding = Expression.Bind(childResultProp, ruleResultTreeArr); + memberInit = Expression.MemberInit(ctor, new[] { rulePropBinding, isSuccessPropBinding, childResultPropBinding, inputBinding }); + } + else if (childRuleResultsblockexpr != null) + { + var childResultPropBinding = Expression.Bind(childResultProp, childRuleResultsblockexpr); + memberInit = Expression.MemberInit(ctor, new[] { rulePropBinding, isSuccessPropBinding, childResultPropBinding, inputBinding }); + } + else + { + memberInit = Expression.MemberInit(ctor, new[] { rulePropBinding, isSuccessPropBinding, inputBinding }); + } + + return memberInit; + } + + /// + /// To the result tree error messages + /// + /// ruleResultTree + /// ruleResultMessage + internal static void ToResultTreeMessages(RuleResultTree ruleResultTree, ref RuleResultMessage ruleResultMessage) + { + if (ruleResultTree.ChildResults != null) + { + GetChildRuleMessages(ruleResultTree.ChildResults, ref ruleResultMessage); + } + else + { + if (ruleResultTree.IsSuccess) + { + string errMsg = ruleResultTree.Rule.ErrorMessage; + errMsg = string.IsNullOrEmpty(errMsg) ? $"Error message does not configured for {ruleResultTree.Rule.RuleName}" : errMsg; + + if (ruleResultTree.Rule.ErrorType == ErrorType.Error && !ruleResultMessage.ErrorMessages.Contains(errMsg)) + { + ruleResultMessage.ErrorMessages.Add(errMsg); + } + else if (ruleResultTree.Rule.ErrorType == ErrorType.Warning && !ruleResultMessage.WarningMessages.Contains(errMsg)) + { + ruleResultMessage.WarningMessages.Add(errMsg); + } + } + } + } + + /// + /// To get the child error message recersivly + /// + /// childResultTree + /// ruleResultMessage + private static void GetChildRuleMessages(IEnumerable childResultTree, ref RuleResultMessage ruleResultMessage) + { + foreach (var item in childResultTree) + { + ToResultTreeMessages(item, ref ruleResultMessage); + } + } + } +} diff --git a/src/RulesEngine/RulesEngine/HelperFunctions/Utils.cs b/src/RulesEngine/RulesEngine/HelperFunctions/Utils.cs new file mode 100644 index 0000000..f22f8a8 --- /dev/null +++ b/src/RulesEngine/RulesEngine/HelperFunctions/Utils.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Linq.Dynamic.Core; + +namespace RulesEngine.HelperFunctions +{ + public static class Utils + { + public static object GetTypedObject(dynamic input) + { + if(input is ExpandoObject) + { + Type type = CreateAbstractClassType(input); + return CreateObject(type, input); + } + else + { + return input; + } + } + public static Type CreateAbstractClassType(dynamic input) + { + List props = new List(); + + if(input == null) + { + return typeof(object); + } + if(!(input is ExpandoObject)) + { + return input.GetType(); + } + + else + { + foreach (var expando in (IDictionary)input) + { + Type value; + if (expando.Value is IList) + { + if (((IList)expando.Value).Count == 0) + value = typeof(List); + else + { + var internalType = CreateAbstractClassType(((IList)expando.Value)[0]); + value = new List().Cast(internalType).ToList(internalType).GetType(); + } + + } + else + { + value = CreateAbstractClassType(expando.Value); + } + props.Add(new DynamicProperty(expando.Key, value)); + } + } + + var type = DynamicClassFactory.CreateType(props); + return type; + } + + public static object CreateObject(Type type, dynamic input) + { + if (!(input is ExpandoObject)) + { + return Convert.ChangeType(input, type); + } + object obj = Activator.CreateInstance(type); + + foreach (var expando in (IDictionary)input) + { + if (type.GetProperties().Any(c => c.Name == expando.Key) && + expando.Value != null && (expando.Value.GetType().Name != "DBNull" || expando.Value != DBNull.Value)) + { + object val; + if (expando.Value is ExpandoObject) + { + var propType = type.GetProperty(expando.Key).PropertyType; + val = CreateObject(propType, expando.Value); + } + else if (expando.Value is IList) + { + var internalType = type.GetProperty(expando.Key).PropertyType.GenericTypeArguments.FirstOrDefault()??typeof(object); + var temp = (IList)expando.Value; + var newList = new List(); + for (int i = 0; i < temp.Count; i++) + { + var child = CreateObject(internalType, temp[i]); + newList.Add(child); + }; + val = newList.Cast(internalType).ToList(internalType); + } + else + { + val = expando.Value; + } + type.GetProperty(expando.Key).SetValue(obj, val, null); + } + + } + + return obj; + } + + private static IEnumerable Cast(this IEnumerable self, Type innerType) + { + var methodInfo = typeof(Enumerable).GetMethod("Cast"); + var genericMethod = methodInfo.MakeGenericMethod(innerType); + return genericMethod.Invoke(null, new[] { self }) as IEnumerable; + } + + private static IList ToList(this IEnumerable self, Type innerType) + { + var methodInfo = typeof(Enumerable).GetMethod("ToList"); + var genericMethod = methodInfo.MakeGenericMethod(innerType); + return genericMethod.Invoke(null, new[] { self }) as IList; + } + } + + +} diff --git a/src/RulesEngine/RulesEngine/ILogger.cs b/src/RulesEngine/RulesEngine/ILogger.cs new file mode 100644 index 0000000..be674bd --- /dev/null +++ b/src/RulesEngine/RulesEngine/ILogger.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace RulesEngine +{ + public interface ILogger + { + void LogTrace(string msg); + void LogError(Exception ex); + } +} diff --git a/src/RulesEngine/RulesEngine/Interfaces/IBusinessRuleEngine.cs b/src/RulesEngine/RulesEngine/Interfaces/IBusinessRuleEngine.cs new file mode 100644 index 0000000..919c080 --- /dev/null +++ b/src/RulesEngine/RulesEngine/Interfaces/IBusinessRuleEngine.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine.Models; +using System.Collections.Generic; + +namespace RulesEngine.Interfaces +{ + public interface IRulesEngine + { + /// + /// This will execute all the rules of the specified workflow + /// + /// + /// + /// + /// List of Result + List ExecuteRule(string workflowName, IEnumerable input, object[] otherInputs); + + + /// + /// This will execute all the rules of the specified workflow + /// + /// + /// + /// List of Result + List ExecuteRule(string workflowName, object[] inputs); + + /// + /// + /// + /// + /// + /// + List ExecuteRule(string workflowName, object input); + + + /// + /// This will execute all the rules of the specified workflow + /// + /// + /// + /// List of Result + List ExecuteRule(string workflowName, RuleParameter[] ruleParams); + } +} diff --git a/src/RulesEngine/RulesEngine/Models/CompiledRule.cs b/src/RulesEngine/RulesEngine/Models/CompiledRule.cs new file mode 100644 index 0000000..e919f1a --- /dev/null +++ b/src/RulesEngine/RulesEngine/Models/CompiledRule.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace RulesEngine.Models +{ + internal class CompiledRule + { + /// + /// Gets or sets the compiled rules. + /// + /// + /// The compiled rules. + /// + internal List CompiledRules { get; set; } + } + +} diff --git a/src/RulesEngine/RulesEngine/Models/ReSettings.cs b/src/RulesEngine/RulesEngine/Models/ReSettings.cs new file mode 100644 index 0000000..33c9449 --- /dev/null +++ b/src/RulesEngine/RulesEngine/Models/ReSettings.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace RulesEngine.Models +{ + [ExcludeFromCodeCoverage] + public class ReSettings + { + public Type[] CustomTypes { get; set; } + } +} diff --git a/src/RulesEngine/RulesEngine/Models/Rule.cs b/src/RulesEngine/RulesEngine/Models/Rule.cs new file mode 100644 index 0000000..0cd6dbc --- /dev/null +++ b/src/RulesEngine/RulesEngine/Models/Rule.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Collections.Generic; + +namespace RulesEngine.Models +{ + /// + /// Rule class + /// + public class Rule + { + /// + /// Gets or sets the name of the rule. + /// + /// + /// The name of the rule. + /// + public string RuleName { get; set; } + + /// + /// Gets or sets the operator. + /// + /// + /// The operator. + /// + public string Operator { get; set; } + + /// + /// Gets or sets the error message. + /// + /// + /// The error message. + /// + public string ErrorMessage { get; set; } + + /// + /// Gets or sets the type of the error. + /// + /// + /// The type of the error. + /// + [JsonConverter(typeof(StringEnumConverter))] + public ErrorType ErrorType { get; set; } + + /// + /// Gets or sets the type of the rule expression. + /// + /// + /// The type of the rule expression. + /// + [JsonConverter(typeof(StringEnumConverter))] + public RuleExpressionType? RuleExpressionType { get; set; } + + + /// + /// Gets or sets the names of common workflows + /// + public List WorkflowRulesToInject { get; set; } + + /// + /// Gets or sets the rules. + /// + /// + /// The rules. + /// + public List Rules { get; set; } + + /// + /// Gets or Sets the lambda expression. + /// + public string Expression { get; set; } + + + public string SuccessEvent { get; set; } + + } + +} diff --git a/src/RulesEngine/RulesEngine/Models/RuleErrorType.cs b/src/RulesEngine/RulesEngine/Models/RuleErrorType.cs new file mode 100644 index 0000000..3d0a262 --- /dev/null +++ b/src/RulesEngine/RulesEngine/Models/RuleErrorType.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace RulesEngine.Models +{ + /// + /// This is error type of rules which will use in rule config files + /// + public enum ErrorType + { + Warning = 0, + Error = 1, + } +} diff --git a/src/RulesEngine/RulesEngine/Models/RuleExpressionType.cs b/src/RulesEngine/RulesEngine/Models/RuleExpressionType.cs new file mode 100644 index 0000000..8029fbf --- /dev/null +++ b/src/RulesEngine/RulesEngine/Models/RuleExpressionType.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace RulesEngine.Models +{ + /// + /// This is rule expression type which will use in rule config files + /// + public enum RuleExpressionType + { + LambdaExpression = 0 + } +} diff --git a/src/RulesEngine/RulesEngine/Models/RuleInput.cs b/src/RulesEngine/RulesEngine/Models/RuleInput.cs new file mode 100644 index 0000000..175533f --- /dev/null +++ b/src/RulesEngine/RulesEngine/Models/RuleInput.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace RulesEngine.Models +{ + /// + /// Rule input + /// + [ExcludeFromCodeCoverage] + internal class RuleInput + { + /// + /// Gets the today UTC. + /// + /// + /// The today UTC. + /// + public DateTime TodayUtc { get; set; } + } +} diff --git a/src/RulesEngine/RulesEngine/Models/RuleParameter.cs b/src/RulesEngine/RulesEngine/Models/RuleParameter.cs new file mode 100644 index 0000000..400943a --- /dev/null +++ b/src/RulesEngine/RulesEngine/Models/RuleParameter.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace RulesEngine.Models +{ + [ExcludeFromCodeCoverage] + public class RuleParameter + { + public RuleParameter(Type type) + { + Type = type; + Name = type.Name; + } + public RuleParameter(Type type,string name) + { + Type = type; + Name = name; + } + + public RuleParameter(string name,object value) + { + Type = value.GetType(); + Name = name; + Value = value; + } + + + public Type Type { get; set; } + public string Name { get; set; } + public object Value { get; set; } + + } +} diff --git a/src/RulesEngine/RulesEngine/Models/RuleResultTree.cs b/src/RulesEngine/RulesEngine/Models/RuleResultTree.cs new file mode 100644 index 0000000..fc00462 --- /dev/null +++ b/src/RulesEngine/RulesEngine/Models/RuleResultTree.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine.HelperFunctions; +using System.Collections.Generic; + +namespace RulesEngine.Models +{ + /// + /// Rule result class with child result heirarchy + /// + public class RuleResultTree + { + /// + /// Gets or sets the rule. + /// + /// + /// The rule. + /// + public Rule Rule { get; set; } + + /// + /// Gets or sets a value indicating whether this instance is success. + /// + /// + /// true if this instance is success; otherwise, false. + /// + public bool IsSuccess { get; set; } + + /// + /// Gets or sets the child result. + /// + /// + /// The child result. + /// + public IEnumerable ChildResults { get; set; } + + /// + /// Gets or sets the input object + /// + public object Input { get; set; } + + /// + /// This method will return all the error and warning messages to caller + /// + /// RuleResultMessage + public RuleResultMessage GetMessages() + { + RuleResultMessage ruleResultMessage = new RuleResultMessage(); + + Helpers.ToResultTreeMessages(this, ref ruleResultMessage); + + return ruleResultMessage; + } + } + + /// + /// This class will hold the error messages + /// + public class RuleResultMessage + { + /// + /// Constructor will innitilaze the List + /// + public RuleResultMessage() + { + ErrorMessages = new List(); + WarningMessages = new List(); + } + + /// + /// This will hold the list of error messages + /// + public List ErrorMessages { get; set; } + + /// + /// This will hold the list of warning messages + /// + public List WarningMessages { get; set; } + } +} diff --git a/src/RulesEngine/RulesEngine/Models/WorkflowRules.cs b/src/RulesEngine/RulesEngine/Models/WorkflowRules.cs new file mode 100644 index 0000000..50cee5a --- /dev/null +++ b/src/RulesEngine/RulesEngine/Models/WorkflowRules.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace RulesEngine.Models +{ + /// + /// Workflow rules class for deserialization the json config file + /// + public class WorkflowRules + { + /// + /// Gets the workflow name. + /// + public string WorkflowName { get; set; } + + public List WorkflowRulesToInject { get; set; } + /// + /// list of rules. + /// + public List Rules { get; set; } + } +} diff --git a/src/RulesEngine/RulesEngine/NullLogger.cs b/src/RulesEngine/RulesEngine/NullLogger.cs new file mode 100644 index 0000000..cae9db8 --- /dev/null +++ b/src/RulesEngine/RulesEngine/NullLogger.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace RulesEngine +{ + internal class NullLogger : ILogger + { + public void LogError(Exception ex) + { + System.Diagnostics.Debug.WriteLine(ex); + Console.WriteLine(ex); + } + + public void LogTrace(string msg) + { + System.Diagnostics.Debug.WriteLine(msg); + Console.WriteLine(msg); + } + } +} diff --git a/src/RulesEngine/RulesEngine/Properties/AssemblyInfo.cs b/src/RulesEngine/RulesEngine/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..ef8ec97 --- /dev/null +++ b/src/RulesEngine/RulesEngine/Properties/AssemblyInfo.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] +[assembly: InternalsVisibleTo("RulesEngine.UnitTest")] diff --git a/src/RulesEngine/RulesEngine/RuleCompiler.cs b/src/RulesEngine/RulesEngine/RuleCompiler.cs new file mode 100644 index 0000000..2f3b79e --- /dev/null +++ b/src/RulesEngine/RulesEngine/RuleCompiler.cs @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine.HelperFunctions; +using RulesEngine.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace RulesEngine +{ + /// + /// Rule compilers + /// + internal class RuleCompiler + { + /// + /// The nested operators + /// + private readonly ExpressionType[] nestedOperators = new ExpressionType[] { ExpressionType.And, ExpressionType.AndAlso, ExpressionType.Or, ExpressionType.OrElse }; + + /// + /// The expression builder factory + /// + private readonly RuleExpressionBuilderFactory _expressionBuilderFactory; + + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The expression builder factory. + /// expressionBuilderFactory + internal RuleCompiler(RuleExpressionBuilderFactory expressionBuilderFactory,ILogger logger) + { + if (expressionBuilderFactory == null) + { + throw new ArgumentNullException($"{nameof(expressionBuilderFactory)} can't be null."); + } + + if (logger == null) + { + throw new ArgumentNullException($"{nameof(logger)} can't be null."); + } + + _logger = logger; + _expressionBuilderFactory = expressionBuilderFactory; + } + + /// + /// Compiles the rule + /// + /// + /// + /// + /// + /// Compiled func delegate + public Delegate CompileRule(Rule rule,params RuleParameter[] ruleParams) + { + try + { + IEnumerable typeParameterExpressions = GetParameterExpression(ruleParams).ToList(); // calling ToList to avoid multiple calls this the method for nested rule scenario. + + ParameterExpression ruleInputExp = Expression.Parameter(typeof(RuleInput), nameof(RuleInput)); + + Expression> ruleExpression = GetExpressionForRule(rule, typeParameterExpressions, ruleInputExp); + + var lambdaParameterExps = new List(typeParameterExpressions) { ruleInputExp }; + + + var expression = Expression.Lambda(ruleExpression.Body, lambdaParameterExps); + + + return expression.Compile(); + } + catch (Exception ex) + { + _logger.LogError(ex); + throw; + } + } + + // + /// Gets the parameter expression. + /// + /// The types. + /// + /// + /// types + /// or + /// type + /// + private IEnumerable GetParameterExpression(params RuleParameter[] ruleParams) + { + if (ruleParams == null || !ruleParams.Any()) + { + throw new ArgumentException($"{nameof(ruleParams)} can't be null/empty."); + } + + foreach (var ruleParam in ruleParams) + { + if (ruleParam == null) + { + throw new ArgumentException($"{nameof(ruleParam)} can't be null."); + } + + yield return Expression.Parameter(ruleParam.Type, ruleParam.Name); + } + } + + /// + /// Gets the expression for rule. + /// + /// The rule. + /// The type parameter expressions. + /// The rule input exp. + /// + private Expression> GetExpressionForRule(Rule rule, IEnumerable typeParameterExpressions, ParameterExpression ruleInputExp) + { + ExpressionType nestedOperator; + + if (Enum.TryParse(rule.Operator, out nestedOperator) && nestedOperators.Contains(nestedOperator) && + rule.Rules != null && rule.Rules.Any()) + { + return BuildNestedExpression(rule, nestedOperator, typeParameterExpressions, ruleInputExp); + } + else + { + return BuildExpression(rule, typeParameterExpressions, ruleInputExp); + } + } + + /// + /// Builds the expression. + /// + /// The rule. + /// The type parameter expressions. + /// The rule input exp. + /// + /// + private Expression> BuildExpression(Rule rule, IEnumerable typeParameterExpressions, ParameterExpression ruleInputExp) + { + if (!rule.RuleExpressionType.HasValue) + { + throw new InvalidOperationException($"RuleExpressionType can not be null for leaf level expressions."); + } + + var ruleExpressionBuilder = _expressionBuilderFactory.RuleGetExpressionBuilder(rule.RuleExpressionType.Value); + + var expression = ruleExpressionBuilder.BuildExpressionForRule(rule, typeParameterExpressions, ruleInputExp); + + return expression; + } + + /// + /// Builds the nested expression. + /// + /// The parent rule. + /// The child rules. + /// The operation. + /// The type parameter expressions. + /// The rule input exp. + /// Expression of func delegate + /// + private Expression> BuildNestedExpression(Rule parentRule, ExpressionType operation, IEnumerable typeParameterExpressions, ParameterExpression ruleInputExp) + { + List>> expressions = new List>>(); + foreach (var r in parentRule.Rules) + { + expressions.Add(GetExpressionForRule(r, typeParameterExpressions, ruleInputExp)); + } + + List childRuleResultTree = new List(); + + foreach (var exp in expressions) + { + var resultMemberInitExpression = exp.Body as MemberInitExpression; + + if (resultMemberInitExpression == null)// assert is a MemberInitExpression + { + throw new InvalidCastException($"expression.Body '{exp.Body}' is not of MemberInitExpression type."); + } + + childRuleResultTree.Add(resultMemberInitExpression); + } + + Expression> nestedExpression = Helpers.ToResultTreeExpression(parentRule, childRuleResultTree, BinaryExpression(expressions, operation), typeParameterExpressions, ruleInputExp); + + return nestedExpression; + } + + /// + /// Binaries the expression. + /// + /// The expressions. + /// Type of the operation. + /// Binary Expression + private BinaryExpression BinaryExpression(IList>> expressions, ExpressionType operationType) + { + if (expressions.Count == 1) + { + return ResolveIsSuccessBinding(expressions.First()); + } + + BinaryExpression nestedBinaryExp = Expression.MakeBinary(operationType, ResolveIsSuccessBinding(expressions[0]), ResolveIsSuccessBinding(expressions[1])); + + for (int i = 2; expressions.Count > i; i++) + { + nestedBinaryExp = Expression.MakeBinary(operationType, nestedBinaryExp, ResolveIsSuccessBinding(expressions[i])); + } + + return nestedBinaryExp; + } + + /// + /// Resolves the is success binding. + /// + /// The expression. + /// Binary expression of IsSuccess prop + /// expression + /// + /// + /// IsSuccess + /// or + /// IsSuccess + /// or + /// IsSuccess + /// + private BinaryExpression ResolveIsSuccessBinding(Expression> expression) + { + if (expression == null) + { + throw new ArgumentNullException($"{nameof(expression)} should not be null."); + } + + var memberInitExpression = expression.Body as MemberInitExpression; + + if (memberInitExpression == null)// assert it's a MemberInitExpression + { + throw new InvalidCastException($"expression.Body '{expression.Body}' is not of MemberInitExpression type."); + } + + MemberAssignment isSuccessBinding = (MemberAssignment)memberInitExpression.Bindings.FirstOrDefault(f => f.Member.Name == nameof(RuleResultTree.IsSuccess)); + + if (isSuccessBinding == null) + { + throw new NullReferenceException($"Expected {nameof(RuleResultTree.IsSuccess)} property binding not found in {memberInitExpression}."); + } + + if (isSuccessBinding.Expression == null) + { + throw new NullReferenceException($"{nameof(RuleResultTree.IsSuccess)} assignment expression can not be null."); + } + + BinaryExpression isSuccessExpression = isSuccessBinding.Expression as BinaryExpression; + + if (isSuccessExpression == null) + { + throw new NullReferenceException($"Expected {nameof(RuleResultTree.IsSuccess)} assignment expression to be of {typeof(BinaryExpression)} and not {isSuccessBinding.Expression.GetType()}"); + } + + return isSuccessExpression; + } + } +} diff --git a/src/RulesEngine/RulesEngine/RuleExpressionBuilderFactory.cs b/src/RulesEngine/RulesEngine/RuleExpressionBuilderFactory.cs new file mode 100644 index 0000000..d5480a6 --- /dev/null +++ b/src/RulesEngine/RulesEngine/RuleExpressionBuilderFactory.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine.ExpressionBuilders; +using RulesEngine.Models; +using System; + +namespace RulesEngine +{ + internal class RuleExpressionBuilderFactory + { + private ReSettings _reSettings; + public RuleExpressionBuilderFactory(ReSettings reSettings) + { + _reSettings = reSettings; + } + public RuleExpressionBuilderBase RuleGetExpressionBuilder(RuleExpressionType ruleExpressionType) + { + switch (ruleExpressionType) + { + case RuleExpressionType.LambdaExpression: + return new LambdaExpressionBuilder(_reSettings); + default: + throw new InvalidOperationException($"{nameof(ruleExpressionType)} has not been supported yet."); + } + } + } +} diff --git a/src/RulesEngine/RulesEngine/RulesEngine.cs b/src/RulesEngine/RulesEngine/RulesEngine.cs new file mode 100644 index 0000000..1b4ed86 --- /dev/null +++ b/src/RulesEngine/RulesEngine/RulesEngine.cs @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine.HelperFunctions; +using RulesEngine.Interfaces; +using RulesEngine.Models; +using RulesEngine.Validators; +using RulesEngine.Exceptions; +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using FluentValidation; + +namespace RulesEngine +{ + public class RulesEngine : IRulesEngine + { + #region Variables + private Dictionary compileRulesDic; + private Dictionary workflowRulesDic; + private readonly ILogger _logger; + private readonly ReSettings _reSettings; + #endregion + + #region Constructor + + + public RulesEngine(string[] jsonConfig, ILogger logger, ReSettings reSettings = null) : this(logger, reSettings) + { + var workflowRules = jsonConfig.Select(item => JsonConvert.DeserializeObject(item)).ToArray(); + AddWorkflow(workflowRules); + } + + public RulesEngine(WorkflowRules[] workflowRules, ILogger logger, ReSettings reSettings = null) : this(logger, reSettings) + { + AddWorkflow(workflowRules); + } + + public RulesEngine(ILogger logger, ReSettings reSettings = null) + { + _logger = logger ?? new NullLogger(); + _reSettings = reSettings ?? new ReSettings(); + InitializeVariables(); + } + #endregion + + #region Public Methods + + /// + /// This will execute all the rules of the specified workflow + /// + /// type of input + /// input + /// Workflow Name + /// List of Result + public List ExecuteRule(string workflowName, IEnumerable input, object[] otherInputs) + { + _logger.LogTrace($"Called ExecuteRule for workflow {workflowName} and count of input {input.Count()}"); + + var result = new List(); + foreach (var item in input) + { + var ruleInputs = new List(); + ruleInputs.Add(item); + if (otherInputs != null) + ruleInputs.AddRange(otherInputs); + result.AddRange(ExecuteRule(workflowName, ruleInputs.ToArray())); + + } + + return result; + } + + public List ExecuteRule(string workflowName, object[] inputs) + { + var ruleParams = new List(); + + for (int i = 0; i < inputs.Length; i++) + { + var input = inputs[i]; + var obj = Utils.GetTypedObject(input); + ruleParams.Add(new RuleParameter($"input{i + 1}", obj)); + } + return ExecuteRule(workflowName, ruleParams.ToArray()); + } + + public List ExecuteRule(string workflowName, object input) + { + var inputs = new[] { input }; + return ExecuteRule(workflowName, inputs); + } + + public List ExecuteRule(string workflowName, RuleParameter[] ruleParams) + { + return ValidateWorkflowAndExecuteRule(workflowName, ruleParams); + } + + #endregion + + #region Private Methods + + /// + /// This is for Initializing the variables + /// + private void InitializeVariables() + { + if (compileRulesDic == null) + compileRulesDic = new Dictionary(); + + if (workflowRulesDic == null) + workflowRulesDic = new Dictionary(); + } + + public void AddWorkflow(params WorkflowRules[] workflowRules) + { + try + { + foreach (var workflowRule in workflowRules) + { + var validator = new WorkflowRulesValidator(); + validator.ValidateAndThrow(workflowRule); + if (!workflowRulesDic.ContainsKey(workflowRule.WorkflowName)) + { + workflowRulesDic[workflowRule.WorkflowName] = workflowRule; + } + else + { + throw new ArgumentException($"Workflow with name: {workflowRule} already exists"); + } + } + } + catch (ValidationException ex) + { + throw new RuleValidationException(ex.Message, ex.Errors); + } + } + + + public void ClearWorkflows() + { + workflowRulesDic.Clear(); + compileRulesDic.Clear(); + } + + public void RemoveWorkflow(params string[] workflowNames) + { + foreach (var workflowName in workflowNames) + { + workflowRulesDic.Remove(workflowName); + var compiledKeysToRemove = compileRulesDic.Keys.Where(key => key.StartsWith(workflowName)); + foreach(var key in compiledKeysToRemove) + { + compileRulesDic.Remove(key); + } + + } + } + + /// + /// This will validate workflow rules then call execute method + /// + /// type of entity + /// input + /// workflow name + /// list of rule result set + private List ValidateWorkflowAndExecuteRule(string workflowName, RuleParameter[] ruleParams) + { + List result; + + if (RegisterRule(workflowName, ruleParams)) + { + result = ExecuteRuleByWorkflow(workflowName, ruleParams); + } + else + { + _logger.LogTrace($"Rule config file is not present for the {workflowName} workflow"); + // if rules are not registered with Rules Engine + throw new ArgumentException($"Rule config file is not present for the {workflowName} workflow"); + } + return result; + } + + + /// + /// This will compile the rules and store them to dictionary + /// + /// type of entity + /// workflow name + /// bool result + private bool RegisterRule(string workflowName, params RuleParameter[] ruleParams) + { + string compileRulesKey = GetCompileRulesKey(workflowName, ruleParams); + if (compileRulesDic.ContainsKey(compileRulesKey)) + return true; + + + var workflowRules = GetWorkFlowRules(workflowName); + + if (workflowRules != null) + { + var lstFunc = new List(); + foreach (var rule in workflowRulesDic[workflowName].Rules) + { + RuleCompiler ruleCompiler = new RuleCompiler(new RuleExpressionBuilderFactory(_reSettings), _logger); + lstFunc.Add(ruleCompiler.CompileRule(rule, ruleParams)); + } + compileRulesDic.Add(compileRulesKey, new CompiledRule() { CompiledRules = lstFunc }); + _logger.LogTrace($"Rules has been compiled for the {workflowName} workflow and added to dictionary"); + return true; + } + else + { + return false; + } + } + + private WorkflowRules GetWorkFlowRules(string workflowName) + { + workflowRulesDic.TryGetValue(workflowName, out var workflowRules); + if (workflowRules == null) return null; + else + { + if (workflowRules.WorkflowRulesToInject?.Any() == true) + { + if (workflowRules.Rules == null) + { + workflowRules.Rules = new List(); + } + foreach (string wfname in workflowRules.WorkflowRulesToInject) + { + var injectedWorkflow = GetWorkFlowRules(wfname); + if (injectedWorkflow == null) + { + throw new Exception($"Could not find injected Workflow: {wfname}"); + } + workflowRules.Rules.AddRange(injectedWorkflow.Rules); + } + } + + return workflowRules; + } + } + + private static string GetCompileRulesKey(string workflowName, RuleParameter[] ruleParams) + { + return $"{workflowName}-" + String.Join("-", ruleParams.Select(c => c.Type.Name)); + } + + /// + /// This will execute the compiled rules + /// + /// + /// + /// list of rule result set + private List ExecuteRuleByWorkflow(string workflowName, RuleParameter[] ruleParams) + { + _logger.LogTrace($"Compiled rules found for {workflowName} workflow and executed"); + + List result = new List(); + var compileRulesKey = GetCompileRulesKey(workflowName, ruleParams); + var inputs = ruleParams.Select(c => c.Value); + foreach (var compiledRule in (compileRulesDic[compileRulesKey] as CompiledRule).CompiledRules) + { + result.Add(compiledRule.DynamicInvoke(new List(inputs) { new RuleInput() }.ToArray()) as RuleResultTree); + } + + return result; + } + #endregion + } +} diff --git a/src/RulesEngine/RulesEngine/RulesEngine.csproj b/src/RulesEngine/RulesEngine/RulesEngine.csproj new file mode 100644 index 0000000..2090856 --- /dev/null +++ b/src/RulesEngine/RulesEngine/RulesEngine.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.0 + + + + + + + + + + + diff --git a/src/RulesEngine/RulesEngine/Validators/RuleValidator.cs b/src/RulesEngine/RulesEngine/Validators/RuleValidator.cs new file mode 100644 index 0000000..b5ff157 --- /dev/null +++ b/src/RulesEngine/RulesEngine/Validators/RuleValidator.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using FluentValidation; +using RulesEngine.HelperFunctions; +using RulesEngine.Models; + +namespace RulesEngine.Validators +{ + internal class RuleValidator : AbstractValidator + { + private readonly List _nestedOperators = new List { ExpressionType.And, ExpressionType.AndAlso, ExpressionType.Or, ExpressionType.OrElse }; + public RuleValidator() + { + RuleFor(c => c.RuleName).NotEmpty().WithMessage(Constants.RULE_NAME_NULL_ERRMSG); + + //Nested expression check + When(c => c.RuleExpressionType == null,() => + { + RuleFor(c => c.Operator) + .NotNull().WithMessage(Constants.OPERATOR_NULL_ERRMSG) + .Must(op => _nestedOperators.Any(x => x.ToString().Equals(op, StringComparison.OrdinalIgnoreCase))) + .WithMessage(Constants.OPERATOR_INCORRECT_ERRMSG); + + When(c => c.Rules?.Any() != true, () => + { + RuleFor(c => c.WorkflowRulesToInject).NotEmpty().WithMessage(Constants.INJECT_WORKFLOW_RULES_ERRMSG); + }) + .Otherwise(() => { + RuleFor(c => c.Rules).Must(BeValidRulesList); + }); + }); + RegisterExpressionTypeRules(); + } + + private void RegisterExpressionTypeRules() + { + When(c => c.RuleExpressionType == RuleExpressionType.LambdaExpression, () => + { + RuleFor(c => c.Expression).NotEmpty().WithMessage(Constants.LAMBDA_EXPRESSION_EXPRESSION_NULL_ERRMSG); + RuleFor(c => c.Operator).Null().WithMessage(Constants.LAMBDA_EXPRESSION_OPERATOR_ERRMSG); + RuleFor(c => c.Rules).Null().WithMessage(Constants.LAMBDA_EXPRESSION_RULES_ERRMSG); + }); + } + + private bool BeValidRulesList(List rules) + { + if (rules?.Any() != true) return false; + var validator = new RuleValidator(); + var isValid = true; + foreach(var rule in rules){ + isValid &= validator.Validate(rule).IsValid; + if (!isValid) break; + } + return isValid; + } + } +} diff --git a/src/RulesEngine/RulesEngine/Validators/WorkflowRulesValidator.cs b/src/RulesEngine/RulesEngine/Validators/WorkflowRulesValidator.cs new file mode 100644 index 0000000..4a8c363 --- /dev/null +++ b/src/RulesEngine/RulesEngine/Validators/WorkflowRulesValidator.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Linq; +using FluentValidation; +using RulesEngine.HelperFunctions; +using RulesEngine.Models; + +namespace RulesEngine.Validators +{ + internal class WorkflowRulesValidator : AbstractValidator + { + public WorkflowRulesValidator() + { + RuleFor(c => c.WorkflowName).NotEmpty().WithMessage(Constants.WORKFLOW_NAME_NULL_ERRMSG); + When(c => c.Rules?.Any() != true, () => + { + RuleFor(c => c.WorkflowRulesToInject).NotEmpty().WithMessage(Constants.INJECT_WORKFLOW_RULES_ERRMSG); + }).Otherwise(() => { + var ruleValidator = new RuleValidator(); + RuleForEach(c => c.Rules).SetValidator(ruleValidator); + }); + } + } +} diff --git a/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs new file mode 100644 index 0000000..cf13e7f --- /dev/null +++ b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine; +using RulesEngine.Exceptions; +using RulesEngine.HelperFunctions; +using RulesEngine.Models; +using Moq; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.IO; +using System.Linq; +using Xunit; + +namespace RulesEngine.UnitTest +{ + [Trait("Category", "Unit")] + public class RulesEngineTest + { + [Theory] + [InlineData("rules1.json")] + public void RulesEngine_New_ReturnsNotNull(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + Assert.NotNull(re); + } + + [Theory] + [InlineData("rules2.json")] + public void RulesEngine_InjectedRules_ReturnsListOfRuleResultTree(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + dynamic input = GetInput(); + + dynamic input2 = GetInput(); + input2.value1 = "val1"; + + var result = re.ExecuteRule("inputWorkflowReference", new List() { input, input2 }.AsEnumerable(), new object[] { }); + Assert.NotNull(result); + Assert.IsType>(result); + } + + [Theory] + [InlineData("rules2.json")] + public void ExecuteRule_ReturnsListOfRuleResultTree(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + dynamic input = GetInput(); + + dynamic input2 = GetInput(); + input2.value1 = "val1"; + + var result = re.ExecuteRule("inputWorkflow", new List() { input, input2 }.AsEnumerable(), new object[] { }); + Assert.NotNull(result); + Assert.IsType>(result); + } + + [Theory] + [InlineData("rules2.json")] + public void ExecuteRule_SingleObject_ReturnsListOfRuleResultTree(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + dynamic input = GetInput(); + + dynamic input2 = GetInput(); + input2.value1 = "val1"; + + var result = re.ExecuteRule("inputWorkflow",input); + Assert.NotNull(result); + Assert.IsType>(result); + } + + [Theory] + [InlineData("rules2.json")] + public void ExecuteRule_ReturnsListOfRuleResultTree_ResultMessage(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + dynamic input = GetInput(); + + dynamic input2 = GetInput(); + input2.value1 = "val1"; + + List result = re.ExecuteRule("inputWorkflow", input); + Assert.NotNull(result); + Assert.IsType>(result); + Assert.NotNull(result.First().GetMessages()); + Assert.NotNull(result.First().GetMessages().WarningMessages); + } + + [Fact] + public void RulesEngine_New_IncorrectJSON_ThrowsException() + { + + Assert.Throws(() => + { + var workflow = new WorkflowRules(); + var re = CreateRulesEngine(workflow); + }); + + + Assert.Throws(() => + { + var workflow = new WorkflowRules() { WorkflowName = "test" }; + var re = CreateRulesEngine(workflow); + }); + + + + } + + + [Theory] + [InlineData("rules1.json")] + public void ExecuteRule_InvalidWorkFlow_ThrowsException(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + dynamic input = GetInput(); + + Assert.Throws(() => { re.ExecuteRule("inputWorkflow1", new List() { input }.AsEnumerable(), new object[] { }); }); + } + + [Theory] + [InlineData("rules1.json")] + [InlineData("rules2.json")] + public void ExecuteRule_InputWithVariableProps_ReturnsResult(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + + dynamic input = GetInput(); + dynamic input2 = GetInput(); + input2.valueX = "hello"; + + var result = re.ExecuteRule("inputWorkflow", new List() { input, input2 }.AsEnumerable(), new object[] { }); + Assert.NotNull(result); + Assert.IsType>(result); + } + + private RulesEngine CreateRulesEngine(WorkflowRules workflow) + { + var json = JsonConvert.SerializeObject(workflow); + return new RulesEngine(new string[] { json }, null); + } + + private RulesEngine GetRulesEngine(string filename) + { + var filePath = Path.Combine(Directory.GetCurrentDirectory() as string, "TestData", filename); + var data = File.ReadAllText(filePath); + + var injectWorkflow = new WorkflowRules + { + WorkflowName = "inputWorkflowReference", + WorkflowRulesToInject = new List { "inputWorkflow" } + }; + + var injectWorkflowStr = JsonConvert.SerializeObject(injectWorkflow); + var mockLogger = new Mock(); + return new RulesEngine(new string[] { data, injectWorkflowStr}, mockLogger.Object); + } + + private dynamic GetInput() + { + dynamic input = new ExpandoObject(); + input.value1 = "value1"; + input.value2 = "value2"; + input.value3 = 1; + input.value4 = new { subValue = "subValue", nullValue = default(string) }; + return input; + } + } +} \ No newline at end of file diff --git a/test/RulesEngine.UnitTest/CustomTypeProviderTests.cs b/test/RulesEngine.UnitTest/CustomTypeProviderTests.cs new file mode 100644 index 0000000..170b3c7 --- /dev/null +++ b/test/RulesEngine.UnitTest/CustomTypeProviderTests.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine; +using Moq; +using System; +using Xunit; + +namespace RulesEngine.UnitTest +{ + [Trait("Category", "Unit")] + public class CustomTypeProviderTests : IDisposable + { + private MockRepository mockRepository; + + + + public CustomTypeProviderTests() + { + this.mockRepository = new MockRepository(MockBehavior.Strict); + + + } + + public void Dispose() + { + this.mockRepository.VerifyAll(); + } + + private CustomTypeProvider CreateProvider() + { + return new CustomTypeProvider(null); + } + + [Fact] + public void GetCustomTypes_StateUnderTest_ExpectedBehavior() + { + // Arrange + var unitUnderTest = this.CreateProvider(); + + // Act + var result = unitUnderTest.GetCustomTypes(); + + // Assert + Assert.NotEmpty(result); + } + } +} diff --git a/test/RulesEngine.UnitTest/ExpressionUtilsTest.cs b/test/RulesEngine.UnitTest/ExpressionUtilsTest.cs new file mode 100644 index 0000000..3872e56 --- /dev/null +++ b/test/RulesEngine.UnitTest/ExpressionUtilsTest.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine.HelperFunctions; +using Xunit; + +namespace RulesEngine.UnitTest +{ + [Trait("Category", "Unit")] + public class ExpressionUtilsTest + { + [Fact] + public void CheckContainsTest() + { + var result = ExpressionUtils.CheckContains("", ""); + Assert.False(result); + + result = ExpressionUtils.CheckContains(null, ""); + Assert.False(result); + + result = ExpressionUtils.CheckContains("4", "1,2,3,4,5"); + Assert.True(result); + + result = ExpressionUtils.CheckContains("6", "1,2,3,4,5"); + Assert.False(result); + } + } +} diff --git a/test/RulesEngine.UnitTest/LambdaExpressionBuilderTest.cs b/test/RulesEngine.UnitTest/LambdaExpressionBuilderTest.cs new file mode 100644 index 0000000..e73ae77 --- /dev/null +++ b/test/RulesEngine.UnitTest/LambdaExpressionBuilderTest.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine; +using RulesEngine.Models; +using System.Collections.Generic; +using System.Linq.Expressions; +using Xunit; + +namespace RulesEngine.UnitTest +{ + [Trait("Category", "Unit")] + public class LambdaExpressionBuilderTest + { + [Fact] + public void BuildExpressionForRuleTest() + { + var objBuilderFactory = new RuleExpressionBuilderFactory(new ReSettings()); + var builder = objBuilderFactory.RuleGetExpressionBuilder(RuleExpressionType.LambdaExpression); + + var parameterExpressions = new List(); + parameterExpressions.Add(Expression.Parameter(typeof(string), "RequestType")); + parameterExpressions.Add(Expression.Parameter(typeof(string), "RequestStatus")); + parameterExpressions.Add(Expression.Parameter(typeof(string), "RegistrationStatus")); + + Rule mainRule = new Rule(); + mainRule.RuleName = "rule1"; + mainRule.Operator = "And"; + mainRule.Rules = new List(); + + Rule dummyRule = new Rule(); + dummyRule.RuleName = "testRule1"; + dummyRule.RuleExpressionType = RuleExpressionType.LambdaExpression; + dummyRule.Expression = "RequestType == \"vod\""; + + mainRule.Rules.Add(dummyRule); + + ParameterExpression ruleInputExp = Expression.Parameter(typeof(RuleInput), nameof(RuleInput)); + + var expression = builder.BuildExpressionForRule(dummyRule, parameterExpressions, ruleInputExp); + + Assert.NotNull(expression); + Assert.Equal(typeof(RuleResultTree), expression.ReturnType); + } + } +} diff --git a/test/RulesEngine.UnitTest/NullLoggerTest.cs b/test/RulesEngine.UnitTest/NullLoggerTest.cs new file mode 100644 index 0000000..f4c1014 --- /dev/null +++ b/test/RulesEngine.UnitTest/NullLoggerTest.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine; +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace RulesEngine.UnitTest +{ + [Trait("Category","Unit")] + public class NullLoggerTest + { + [Fact] + public void NullLogger_LogTrace() + { + var logger = new NullLogger(); + logger.LogTrace("hello"); + } + + + [Fact] + public void NullLogger_LogError() + { + var logger = new NullLogger(); + logger.LogError(new Exception("hello")); + } + } +} diff --git a/test/RulesEngine.UnitTest/RuleCompilerTest.cs b/test/RulesEngine.UnitTest/RuleCompilerTest.cs new file mode 100644 index 0000000..62d25a1 --- /dev/null +++ b/test/RulesEngine.UnitTest/RuleCompilerTest.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine; +using RulesEngine.Models; +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace RulesEngine.UnitTest +{ + [Trait("Category","Unit")] + public class RuleCompilerTest + { + [Fact] + public void RuleCompiler_NullCheck() + { + Assert.Throws(() => new RuleCompiler(null, null)); + Assert.Throws(() => new RuleCompiler(new RuleExpressionBuilderFactory(new ReSettings()), null)); + } + + [Fact] + public void RuleCompiler_CompileRule_ThrowsException() + { + var compiler = new RuleCompiler(new RuleExpressionBuilderFactory(new ReSettings()), new NullLogger()); + Assert.Throws(() => compiler.CompileRule(null, null)); + Assert.Throws(() => compiler.CompileRule(null, new RuleParameter[] { null})); + } + + + } +} diff --git a/test/RulesEngine.UnitTest/RuleExpressionBuilderFactoryTest.cs b/test/RulesEngine.UnitTest/RuleExpressionBuilderFactoryTest.cs new file mode 100644 index 0000000..dde2e2c --- /dev/null +++ b/test/RulesEngine.UnitTest/RuleExpressionBuilderFactoryTest.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine; +using RulesEngine.ExpressionBuilders; +using RulesEngine.Models; +using System; +using Xunit; + +namespace RulesEngine.UnitTest +{ + [Trait("Category", "Unit")] + public class RuleExpressionBuilderFactoryTest + { + [Theory] + [InlineData(RuleExpressionType.LambdaExpression, typeof(LambdaExpressionBuilder))] + public void RuleGetExpressionBuilderTest(RuleExpressionType expressionType, Type expectedExpressionBuilderType) + { + var objBuilderFactory = new RuleExpressionBuilderFactory(new ReSettings()); + var builder = objBuilderFactory.RuleGetExpressionBuilder(expressionType); + + var builderType = builder.GetType(); + Assert.Equal(expectedExpressionBuilderType.ToString(), builderType.ToString()); + } + } +} diff --git a/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj b/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj new file mode 100644 index 0000000..06b40a5 --- /dev/null +++ b/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj @@ -0,0 +1,31 @@ + + + + Exe + netcoreapp3.0 + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/test/RulesEngine.UnitTest/TestData/rules1.json b/test/RulesEngine.UnitTest/TestData/rules1.json new file mode 100644 index 0000000..53acad6 --- /dev/null +++ b/test/RulesEngine.UnitTest/TestData/rules1.json @@ -0,0 +1,20 @@ +{ + "WorkflowName": "inputWorkflow", + "Rules": [ + { + "RuleName": "Rule1", + "Operator": "AndAlso", + "ErrorMessage": "One or more adjust rules failed.", + "ErrorType": "Error", + "Rules": [ + { + "RuleName": "SubRule1", + "Expression": "input1.Request_RequestType == \"vod\" AND input1.Labor_BillingCode == \"billable\" AND ((input1.Request_RegistrationStatus == \"cancelled with t&e\" AND input1.Request_Status == \"cancelled\") OR (input1.Request_Status != \"cancelled\"))", + "ErrorMessage": "SubError message 1", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression" + } + ] + } + ] +} diff --git a/test/RulesEngine.UnitTest/TestData/rules2.json b/test/RulesEngine.UnitTest/TestData/rules2.json new file mode 100644 index 0000000..9ea5aa1 --- /dev/null +++ b/test/RulesEngine.UnitTest/TestData/rules2.json @@ -0,0 +1,34 @@ +{ + "WorkflowName": "inputWorkflow", + "Rules": [ + { + "RuleName": "Rule1", + "Operator": "Or", + "ErrorMessage": "One or more adjust rules failed.", + "ErrorType": "Error", + "Rules": [ + { + "RuleName": "SubRule1", + "Expression": "input1.Request_RequestType == \"vod\" AND input1.Labor_BillingCode == \"billable\" AND ((input1.Request_RegistrationStatus == \"cancelled with t&e\" AND input1.Request_Status == \"cancelled\") OR (input1.Request_Status != \"cancelled\"))", + "ErrorMessage": "SubError message 1", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression" + }, + { + "RuleName": "SubRule2", + "Expression": "1 == 1", + "ErrorMessage": "SubError message 2", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression" + }, + { + "RuleName": "SubRule3", + "Expression": "input1.Request_RequestType == \"vod\" AND input1.Labor_BillingCode == \"billable\" AND ((input1.Request_RegistrationStatus == \"cancelled with t&e\" AND input1.Request_Status == \"cancelled\") OR (input1.Request_Status != \"cancelled\"))", + "ErrorMessage": "SubError message 3", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression" + } + ] + } + ] +} diff --git a/test/RulesEngine.UnitTest/UtilsTests.cs b/test/RulesEngine.UnitTest/UtilsTests.cs new file mode 100644 index 0000000..58a73e9 --- /dev/null +++ b/test/RulesEngine.UnitTest/UtilsTests.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine.HelperFunctions; +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Text; +using Xunit; + +namespace RulesEngine.UnitTest +{ + public class TestClass + { + public string test { get; set; } + public List testList { get; set; } + } + + [Trait("Category","Unit")] + public class UtilsTests + { + + [Fact] + public void GetTypedObject_dynamicObject() + { + dynamic obj = new ExpandoObject(); + obj.test = "hello"; + obj.testList = new List { 1, 2, 3 }; + object typedobj = Utils.GetTypedObject(obj); + Assert.IsNotType(typedobj); + Assert.NotNull(typedobj.GetType().GetProperty("test")); + } + + [Fact] + public void GetTypedObject_nonDynamicObject() + { + var obj = new { + test = "hello" + }; + object typedobj = Utils.GetTypedObject(obj); + Assert.IsNotType(typedobj); + Assert.NotNull(typedobj.GetType().GetProperty("test")); + } + + [Fact] + public void CreateObject_dynamicObject() + { + dynamic obj = new ExpandoObject(); + obj.test = "test"; + obj.testList = new List { 1, 2, 3 }; + + object newObj = Utils.CreateObject(typeof(TestClass), obj); + Assert.IsNotType(newObj); + Assert.NotNull(newObj.GetType().GetProperty("test")); + + } + + [Fact] + public void CreateAbstractType_dynamicObject() + { + dynamic obj = new ExpandoObject(); + obj.test = "test"; + obj.testList = new List { 1, 2, 3 }; + obj.testEmptyList = new List(); + + Type type = Utils.CreateAbstractClassType( obj); + Assert.NotEqual(typeof(ExpandoObject), type); + Assert.NotNull(type.GetProperty("test")); + + } + + + } +}