├── README.md ├── .gitignore ├── EngineTests ├── TestModelWithInterfaces │ ├── Product2.cs │ ├── IProduct.cs │ ├── ICdsTrade.cs │ ├── CdsTrade2.cs │ ├── CdsTrade.cs │ ├── ICreditDefaultSwap.cs │ ├── CreditDefaultSwap.cs │ ├── ITrade.cs │ ├── CreditDefaultSwap2.cs │ ├── Trade.cs │ ├── Trade2.cs │ ├── CdsRules2.cs │ └── CdsRules.cs ├── TestModelWithoutInterfaces │ ├── IProduct.cs │ ├── CdsTrade.cs │ ├── CreditDefaultSwap.cs │ ├── Trade.cs │ └── CdsRules.cs ├── IBingo.cs ├── IAbcd.cs ├── Bingo.cs ├── Xyz.cs ├── Abcd.cs ├── Dog.cs ├── XyzRules.cs ├── EngineTests.csproj ├── AbcdRules.cs ├── CompositeObjectsWithoutInterfacesTestFixture.cs ├── DogRules.cs ├── BingoRules.cs ├── DynamicWrapperTestFixture.cs ├── CompositeObjectsWithInterfacesTestFixture.cs ├── InterfaceWrapperTestFixture.cs ├── RulesEngineTestFixture.cs ├── UtilitiesTestFixture.cs └── ExplainTestFixture.cs ├── .github └── workflows │ └── dotnetcore.yml ├── BusinessRulesEngine.sln.DotSettings ├── RulesEngine ├── RulesEngine │ ├── Explain │ │ ├── ExplainExpression.cs │ │ ├── NullQueryable.cs │ │ ├── NullExecutor.cs │ │ ├── QueryOperator.cs │ │ ├── ExplainOrExpression.cs │ │ ├── ExplainAndExpression.cs │ │ ├── Explain.cs │ │ ├── LeafExpression.cs │ │ └── QueryVisitor.cs │ ├── Rule.cs │ ├── MappingRules.cs │ └── FluentExtensions.cs ├── RulesEngine.csproj ├── Tools │ ├── RuntimeCompiler.cs │ ├── ExpressionTreeHelper.cs │ └── CompiledAccessors.cs └── Interceptors │ ├── InterfaceWrapper.cs │ └── DynamicWrapper.cs └── BusinessRulesEngine.sln /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usinesoft/BusinessRulesEngine/HEAD/README.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usinesoft/BusinessRulesEngine/HEAD/.gitignore -------------------------------------------------------------------------------- /EngineTests/TestModelWithInterfaces/Product2.cs: -------------------------------------------------------------------------------- 1 | namespace EngineTests.TestModelWithInterfaces 2 | { 3 | public abstract class Product2 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /EngineTests/TestModelWithInterfaces/IProduct.cs: -------------------------------------------------------------------------------- 1 | namespace EngineTests.TestModelWithInterfaces 2 | { 3 | public interface IProduct 4 | { 5 | string InstrumentName { get; } 6 | } 7 | } -------------------------------------------------------------------------------- /EngineTests/TestModelWithoutInterfaces/IProduct.cs: -------------------------------------------------------------------------------- 1 | namespace EngineTests.TestModelWithoutInterfaces 2 | { 3 | public interface IProduct 4 | { 5 | string InstrumentName { get; } 6 | } 7 | } -------------------------------------------------------------------------------- /EngineTests/IBingo.cs: -------------------------------------------------------------------------------- 1 | namespace EngineTests 2 | { 3 | public interface IBingo 4 | { 5 | int X { get; set; } 6 | int Y { get; set; } 7 | string Message { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /EngineTests/TestModelWithInterfaces/ICdsTrade.cs: -------------------------------------------------------------------------------- 1 | namespace EngineTests.TestModelWithInterfaces 2 | { 3 | public interface ICdsTrade : ITrade 4 | { 5 | ICreditDefaultSwap CdsProduct { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /EngineTests/IAbcd.cs: -------------------------------------------------------------------------------- 1 | namespace EngineTests 2 | { 3 | public interface IAbcd 4 | { 5 | int A { get; set; } 6 | int B { get; set; } 7 | int C { get; set; } 8 | int D { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /EngineTests/Bingo.cs: -------------------------------------------------------------------------------- 1 | namespace EngineTests 2 | { 3 | public class Bingo : IBingo 4 | { 5 | public int X { get; set; } 6 | 7 | public int Y { get; set; } 8 | 9 | public string Message { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /EngineTests/Xyz.cs: -------------------------------------------------------------------------------- 1 | namespace EngineTests 2 | { 3 | public class Xyz 4 | { 5 | public virtual int X { get; set; } 6 | public virtual int Y { get; set; } 7 | public virtual int Z { get; set; } 8 | 9 | } 10 | } -------------------------------------------------------------------------------- /EngineTests/Abcd.cs: -------------------------------------------------------------------------------- 1 | namespace EngineTests 2 | { 3 | public class Abcd : IAbcd 4 | { 5 | public int A { get; set; } 6 | public int B { get; set; } 7 | public int C { get; set; } 8 | public int D { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /EngineTests/TestModelWithoutInterfaces/CdsTrade.cs: -------------------------------------------------------------------------------- 1 | namespace EngineTests.TestModelWithoutInterfaces 2 | { 3 | public class CdsTrade : Trade 4 | { 5 | public CreditDefaultSwap CdsProduct 6 | { 7 | get => (CreditDefaultSwap) Product; 8 | 9 | set => Product = value; 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /EngineTests/TestModelWithInterfaces/CdsTrade2.cs: -------------------------------------------------------------------------------- 1 | namespace EngineTests.TestModelWithInterfaces 2 | { 3 | public class CdsTrade2 : Trade2 4 | { 5 | public virtual CreditDefaultSwap2 CdsProduct 6 | { 7 | get => (CreditDefaultSwap2) Product; 8 | 9 | set => Product = value; 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /EngineTests/Dog.cs: -------------------------------------------------------------------------------- 1 | namespace EngineTests 2 | { 3 | public class Dog 4 | { 5 | public bool IsAnimal { get; set; } 6 | public bool IsDangerous { get; set; } 7 | 8 | public string Name { get; set; } 9 | 10 | public int Age { get; set; } 11 | 12 | public string FavoriteToy { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /EngineTests/TestModelWithInterfaces/CdsTrade.cs: -------------------------------------------------------------------------------- 1 | namespace EngineTests.TestModelWithInterfaces 2 | { 3 | public class CdsTrade : Trade, ICdsTrade 4 | { 5 | public ICreditDefaultSwap CdsProduct 6 | { 7 | get { return (ICreditDefaultSwap) Product; } 8 | 9 | set { Product = value; } 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.github/workflows/dotnetcore.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Setup .NET Core 13 | uses: actions/setup-dotnet@v1 14 | with: 15 | dotnet-version: 3.1.100 16 | - name: Build with dotnet 17 | run: dotnet build --configuration Release 18 | -------------------------------------------------------------------------------- /BusinessRulesEngine.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | True -------------------------------------------------------------------------------- /RulesEngine/RulesEngine/Explain/ExplainExpression.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace RulesEngine.RulesEngine.Explain 3 | { 4 | /// 5 | /// Abstract base class for the queries 6 | /// 7 | 8 | public abstract class ExplainExpression 9 | { 10 | /// 11 | /// Check if the current query is valid. Validity rules are specific to each subclass 12 | /// 13 | public abstract bool IsValid { get; } 14 | 15 | } 16 | } -------------------------------------------------------------------------------- /EngineTests/XyzRules.cs: -------------------------------------------------------------------------------- 1 | using RulesEngine.RulesEngine; 2 | 3 | namespace EngineTests 4 | { 5 | public class XyzRules : MappingRules 6 | { 7 | public XyzRules() 8 | { 9 | Set(x => x.Y) 10 | .With(x => x.X * 2) 11 | .OnChanged(x => x.X); 12 | 13 | 14 | Set(x => x.Z) 15 | .With(x => x.Y * 2) 16 | .OnChanged(x => x.Y); 17 | 18 | 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /EngineTests/EngineTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /EngineTests/TestModelWithInterfaces/ICreditDefaultSwap.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EngineTests.TestModelWithInterfaces 4 | { 5 | public interface ICreditDefaultSwap : IProduct 6 | { 7 | DateTime? MaturityDate { get; set; } 8 | 9 | string RefEntity { get; set; } 10 | 11 | string Tenor { get; set; } 12 | 13 | decimal Spread { get; set; } 14 | 15 | decimal Nominal { get; set; } 16 | 17 | string Currency { get; set; } 18 | string Seniority { get; set; } 19 | string Restructuring { get; set; } 20 | string TransactionType { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /EngineTests/TestModelWithoutInterfaces/CreditDefaultSwap.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EngineTests.TestModelWithoutInterfaces 4 | { 5 | public class CreditDefaultSwap : IProduct 6 | { 7 | public DateTime? MaturityDate { get; set; } 8 | 9 | public string RefEntity { get; set; } 10 | 11 | public string Tenor { get; set; } 12 | 13 | public decimal Spread { get; set; } 14 | 15 | public decimal Nominal { get; set; } 16 | 17 | public string Currency { get; set; } 18 | 19 | public string Seniority { get; set; } 20 | 21 | public string Restructuring { get; set; } 22 | 23 | public string TransactionType { get; set; } 24 | public string InstrumentName { get; private set; } 25 | } 26 | } -------------------------------------------------------------------------------- /EngineTests/TestModelWithInterfaces/CreditDefaultSwap.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EngineTests.TestModelWithInterfaces 4 | { 5 | public class CreditDefaultSwap : ICreditDefaultSwap 6 | { 7 | public string InstrumentName { get; private set; } 8 | 9 | public DateTime? MaturityDate { get; set; } 10 | 11 | public string RefEntity { get; set; } 12 | 13 | public string Tenor { get; set; } 14 | 15 | public decimal Spread { get; set; } 16 | 17 | public decimal Nominal { get; set; } 18 | 19 | public string Currency { get; set; } 20 | 21 | public string Seniority { get; set; } 22 | 23 | public string Restructuring { get; set; } 24 | 25 | public string TransactionType { get; set; } 26 | } 27 | } -------------------------------------------------------------------------------- /EngineTests/TestModelWithInterfaces/ITrade.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EngineTests.TestModelWithInterfaces 4 | { 5 | public interface ITrade 6 | { 7 | string CounterpartyRole { get; set; } 8 | 9 | DateTime? TradeDate { get; set; } 10 | 11 | DateTime? ValueDate { get; set; } 12 | 13 | string Counterparty { get; set; } 14 | 15 | string ContractId { get; set; } 16 | 17 | string Folder { get; set; } 18 | 19 | string BrokerParty { get; set; } 20 | 21 | string Trader { get; set; } 22 | 23 | string Sales { get; set; } 24 | 25 | string ClearingHouse { get; set; } 26 | 27 | bool MandatoryClearing { get; set; } 28 | 29 | IProduct Product { get; set; } 30 | 31 | decimal SalesCredit { get; set; } 32 | } 33 | } -------------------------------------------------------------------------------- /RulesEngine/RulesEngine/Explain/NullQueryable.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Linq.Expressions; 3 | using Remotion.Linq; 4 | using Remotion.Linq.Parsing.Structure; 5 | 6 | namespace RulesEngine.RulesEngine.Explain 7 | { 8 | internal class NullQueryable : QueryableBase 9 | { 10 | public NullQueryable(IQueryParser queryParser, IQueryExecutor executor) : base(queryParser, executor) 11 | { 12 | } 13 | 14 | public NullQueryable(IQueryProvider provider) : base(provider) 15 | { 16 | } 17 | 18 | public NullQueryable(IQueryProvider provider, Expression expression) : base(provider, expression) 19 | { 20 | } 21 | 22 | public NullQueryable(IQueryExecutor executor) : base(QueryParser.CreateDefault(), executor) 23 | { 24 | } 25 | 26 | 27 | } 28 | } -------------------------------------------------------------------------------- /EngineTests/AbcdRules.cs: -------------------------------------------------------------------------------- 1 | using RulesEngine.RulesEngine; 2 | 3 | namespace EngineTests 4 | { 5 | public class AbcdRules : MappingRules 6 | { 7 | public AbcdRules() 8 | { 9 | Set(x => x.B) 10 | .With(x => x.A) 11 | .If(x => x.A < 100) 12 | .OnChanged(x => x.A); 13 | 14 | 15 | Set(x => x.C) 16 | .With(x => x.B) 17 | .If(x => x.C < 100) 18 | .OnChanged(x => x.B); 19 | 20 | 21 | Set(x => x.D) 22 | .With(x => x.C) 23 | .If(x => x.D < 100) 24 | .OnChanged(x => x.C); 25 | 26 | 27 | Set(x => x.A) 28 | .With(x => x.D + 1) 29 | .If(x => x.A < 100) 30 | .OnChanged(x => x.D); 31 | 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /EngineTests/TestModelWithInterfaces/CreditDefaultSwap2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EngineTests.TestModelWithInterfaces 4 | { 5 | public class CreditDefaultSwap2:Product2 6 | { 7 | public virtual string InstrumentName 8 | { 9 | get => "CDS"; 10 | } 11 | 12 | public virtual DateTime? MaturityDate { get; set; } 13 | 14 | public virtual string RefEntity { get; set; } 15 | 16 | public virtual string Tenor { get; set; } 17 | 18 | public virtual decimal Spread { get; set; } 19 | 20 | public virtual decimal Nominal { get; set; } 21 | 22 | public virtual string Currency { get; set; } 23 | 24 | public virtual string Seniority { get; set; } 25 | 26 | public virtual string Restructuring { get; set; } 27 | 28 | public virtual string TransactionType { get; set; } 29 | } 30 | } -------------------------------------------------------------------------------- /EngineTests/TestModelWithoutInterfaces/Trade.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EngineTests.TestModelWithoutInterfaces 4 | { 5 | public class Trade 6 | { 7 | public string CounterpartyRole { get; set; } 8 | 9 | public DateTime? TradeDate { get; set; } 10 | 11 | public DateTime? ValueDate { get; set; } 12 | 13 | public string Counterparty { get; set; } 14 | 15 | public string ContractId { get; set; } 16 | 17 | public string Folder { get; set; } 18 | 19 | public string BrokerParty { get; set; } 20 | 21 | public string Trader { get; set; } 22 | 23 | public string Sales { get; set; } 24 | 25 | public string ClearingHouse { get; set; } 26 | 27 | public bool MandatoryClearing { get; set; } 28 | 29 | public IProduct Product { get; set; } 30 | 31 | public decimal SalesCredit { get; set; } 32 | } 33 | } -------------------------------------------------------------------------------- /EngineTests/TestModelWithInterfaces/Trade.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EngineTests.TestModelWithInterfaces 4 | { 5 | public class Trade : ITrade 6 | { 7 | public string CounterpartyRole { get; set; } 8 | 9 | 10 | public DateTime? TradeDate { get; set; } 11 | 12 | public DateTime? ValueDate { get; set; } 13 | 14 | public string Counterparty { get; set; } 15 | 16 | public string ContractId { get; set; } 17 | 18 | public string Folder { get; set; } 19 | 20 | public string BrokerParty { get; set; } 21 | 22 | public string Trader { get; set; } 23 | 24 | public string Sales { get; set; } 25 | 26 | public string ClearingHouse { get; set; } 27 | 28 | public bool MandatoryClearing { get; set; } 29 | 30 | public IProduct Product { get; set; } 31 | 32 | public decimal SalesCredit { get; set; } 33 | } 34 | } -------------------------------------------------------------------------------- /EngineTests/CompositeObjectsWithoutInterfacesTestFixture.cs: -------------------------------------------------------------------------------- 1 | using EngineTests.TestModelWithoutInterfaces; 2 | using NUnit.Framework; 3 | using RulesEngine.Interceptors; 4 | 5 | namespace EngineTests 6 | { 7 | [TestFixture] 8 | public class CompositeObjectsWithoutInterfacesTestFixture 9 | { 10 | [Test] 11 | public void Counterparty_change_fills_product() 12 | { 13 | var trade = new CdsTrade 14 | { 15 | Product = new CreditDefaultSwap() 16 | }; 17 | 18 | dynamic p = new DynamicWrapper(trade, new CdsRules()); 19 | 20 | p.CdsProduct.RefEntity = "AXA"; 21 | 22 | p.Counterparty = "CHASEOTC"; 23 | 24 | Assert.AreEqual("ICEURO", trade.ClearingHouse); 25 | Assert.AreEqual("MMR", trade.CdsProduct.Restructuring); 26 | Assert.AreEqual("SNR", trade.CdsProduct.Seniority); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /RulesEngine/RulesEngine/Explain/NullExecutor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Remotion.Linq; 4 | 5 | namespace RulesEngine.RulesEngine.Explain 6 | { 7 | public class NullExecutor : IQueryExecutor 8 | { 9 | 10 | 11 | public ExplainOrExpression Expression { get; private set; } 12 | 13 | public T ExecuteScalar(QueryModel queryModel) 14 | { 15 | return default(T); 16 | } 17 | 18 | public T ExecuteSingle(QueryModel queryModel, bool returnDefaultWhenEmpty) 19 | { 20 | return default(T); 21 | } 22 | 23 | public IEnumerable ExecuteCollection(QueryModel queryModel) 24 | { 25 | var visitor = new QueryVisitor(); 26 | 27 | visitor.VisitQueryModel(queryModel); 28 | 29 | Expression = visitor.RootExpression; 30 | 31 | return Enumerable.Empty(); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /EngineTests/TestModelWithInterfaces/Trade2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EngineTests.TestModelWithInterfaces 4 | { 5 | public class Trade2 6 | { 7 | public virtual string CounterpartyRole { get; set; } 8 | 9 | 10 | public virtual DateTime? TradeDate { get; set; } 11 | 12 | public virtual DateTime? ValueDate { get; set; } 13 | 14 | public virtual string Counterparty { get; set; } 15 | 16 | public virtual string ContractId { get; set; } 17 | 18 | public virtual string Folder { get; set; } 19 | 20 | public virtual string BrokerParty { get; set; } 21 | 22 | public virtual string Trader { get; set; } 23 | 24 | public virtual string Sales { get; set; } 25 | 26 | public virtual string ClearingHouse { get; set; } 27 | 28 | public virtual bool MandatoryClearing { get; set; } 29 | 30 | public virtual Product2 Product { get; set; } 31 | 32 | public virtual decimal SalesCredit { get; set; } 33 | } 34 | } -------------------------------------------------------------------------------- /RulesEngine/RulesEngine/Explain/QueryOperator.cs: -------------------------------------------------------------------------------- 1 | namespace RulesEngine.RulesEngine.Explain 2 | { 3 | /// 4 | /// Operators to be used in 5 | /// On the server side these operators can be directly applied to indexes 6 | /// 7 | public enum QueryOperator 8 | { 9 | /// 10 | /// Equality operator(==) 11 | /// 12 | Eq, 13 | 14 | /// 15 | /// Greater 16 | /// 17 | Gt, 18 | 19 | /// 20 | /// Greater or Equal 21 | /// 22 | Ge, 23 | 24 | /// 25 | /// Less 26 | /// 27 | Lt, 28 | 29 | /// 30 | /// Less or equal 31 | /// 32 | Le, 33 | 34 | 35 | /// 36 | /// Applies to list keys 37 | /// 38 | In, 39 | 40 | NotEqual 41 | } 42 | } -------------------------------------------------------------------------------- /EngineTests/DogRules.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using RulesEngine.RulesEngine; 3 | 4 | namespace EngineTests 5 | { 6 | public class DogRules : MappingRules 7 | { 8 | public DogRules() 9 | { 10 | 11 | Set(d => d.IsAnimal).With(d => true).OnChanged(d=>d.IsAnimal); 12 | 13 | Set(d=>d.Name).With(d=> "mr. " + d.Name ).If(x=> x.Name != "Clara" && !x.Name.StartsWith("mr.")).OnChanged(d=>d.Name); 14 | 15 | Set(d=>d.IsDangerous).With(d=>d.Age > 3 && d.Name != "Fluffy" ).OnChanged(d=>d.Age); 16 | 17 | Set(d=>d.FavoriteToy).With(d=>GetFavoriteToy(), "find him a toy").If(d=>d.FavoriteToy == null, "he does not already have one").OnChanged(d=>d.FavoriteToy); 18 | 19 | } 20 | 21 | static string GetFavoriteToy() 22 | { 23 | return "ball"; 24 | } 25 | 26 | protected override void Trace(Rule triggeredRule, string triggerProperty, Dog instance) 27 | { 28 | Console.WriteLine(triggeredRule); 29 | Console.WriteLine($"triggered by {triggerProperty}"); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /EngineTests/BingoRules.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using RulesEngine.RulesEngine; 3 | 4 | namespace EngineTests 5 | { 6 | public class BingoRules : MappingRules 7 | { 8 | public BingoRules() 9 | { 10 | Set(i => i.X) 11 | .With(i => i.Y + 1) 12 | .If(i => i.X < 100) 13 | .OnChanged(i => i.Y); 14 | 15 | 16 | Set(i => i.Y) 17 | .With(i => i.X + 1) 18 | .If(i => i.Y < 100) 19 | .OnChanged(i => i.X); 20 | 21 | 22 | Set(i => i.Message) 23 | .With(i => "BINGO") 24 | .If(i => i.X >= 100 || i.Y >= 100) 25 | .OnChanged(i => i.X, i=>i.Y); 26 | 27 | } 28 | 29 | #region Overrides of MappingRules 30 | 31 | protected override void Trace(Rule triggeredRule, string triggerProperty, IBingo instance) 32 | { 33 | Console.WriteLine("{0, 10} : {1}", triggerProperty, triggeredRule); 34 | } 35 | 36 | #endregion 37 | } 38 | } -------------------------------------------------------------------------------- /RulesEngine/RulesEngine.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;netstandard2.1 5 | true 6 | BusinessRules.Engine 7 | 1.0.14 8 | Dan Ionescu 9 | Usinesoft 10 | A rules engine for .Net applications 11 | Copyright USINESOFT 2015 12 | Apache-2.0 13 | https://github.com/usinesoft/BusinessRulesEngine 14 | business rules engine enrichment financial 15 | Provide optional explanation for rules. Multi-target support 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /RulesEngine/RulesEngine/Explain/ExplainOrExpression.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | #endregion 8 | 9 | namespace RulesEngine.RulesEngine.Explain 10 | { 11 | /// 12 | /// A list of and queries bound by an OR operator 13 | /// 14 | 15 | public class ExplainOrExpression : ExplainExpression 16 | { 17 | private readonly List _elements = new List(); 18 | 19 | public override bool IsValid 20 | { 21 | get { return Elements.All(element => element.IsValid); } 22 | } 23 | 24 | /// 25 | /// The elements of type 26 | /// 27 | public IList Elements => _elements; 28 | 29 | 30 | public bool MultipleWhereClauses { get; set; } 31 | 32 | 33 | public override string ToString() 34 | { 35 | if (_elements.Count == 0) 36 | return ""; 37 | 38 | var sb = new StringBuilder(); 39 | sb.Append(string.Join(" OR ",_elements.Select(e => e.ToString()).ToArray())); 40 | 41 | return sb.ToString(); 42 | } 43 | 44 | } 45 | } -------------------------------------------------------------------------------- /RulesEngine/RulesEngine/Explain/ExplainAndExpression.cs: -------------------------------------------------------------------------------- 1 | #region 2 | 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | #endregion 8 | 9 | namespace RulesEngine.RulesEngine.Explain 10 | { 11 | /// 12 | /// A list of atomic queries bound by an AND operator 13 | /// 14 | public class ExplainAndExpression : ExplainExpression 15 | { 16 | /// 17 | /// Create an empty query (called internally by the query builder) 18 | /// 19 | public ExplainAndExpression() 20 | { 21 | Elements = new List(); 22 | } 23 | 24 | /// 25 | /// The contained atomic queries should apply to different keys 26 | /// 27 | public override bool IsValid 28 | { 29 | get { return Elements.All(atomicQuery => atomicQuery.IsValid); } 30 | } 31 | 32 | /// 33 | /// Accessor for the underlying elements ( 34 | /// 35 | public List Elements { get; private set; } 36 | 37 | 38 | public override string ToString() 39 | { 40 | if (Elements.Count == 0) 41 | return ""; 42 | if (Elements.Count == 1) 43 | return Elements[0].ToString(); 44 | 45 | var sb = new StringBuilder(); 46 | sb.Append(string.Join(" AND ", Elements.Select(e => e.ToString()).ToArray())); 47 | 48 | return sb.ToString(); 49 | } 50 | 51 | 52 | } 53 | } -------------------------------------------------------------------------------- /EngineTests/DynamicWrapperTestFixture.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel; 3 | using NUnit.Framework; 4 | using RulesEngine.Interceptors; 5 | 6 | namespace EngineTests 7 | { 8 | [TestFixture] 9 | public class DynamicWrapperTestFixture 10 | { 11 | [Test] 12 | public void Intercept_changes_with_dynamic_wrapper() 13 | { 14 | { 15 | var instance = new Abcd(); 16 | 17 | dynamic abcd = new DynamicWrapper(instance, new AbcdRules()); 18 | 19 | var inotify = (INotifyPropertyChanged) abcd; 20 | 21 | var changed = new List(); 22 | 23 | inotify.PropertyChanged += (sender, args) => changed.Add(args.PropertyName); 24 | 25 | abcd.A = 1; 26 | 27 | Assert.AreEqual(100, abcd.A); 28 | Assert.AreEqual(100, instance.A); 29 | Assert.AreEqual(4, changed.Count); 30 | } 31 | 32 | { 33 | var instance = new Bingo(); 34 | 35 | dynamic bingo = new DynamicWrapper(instance, new BingoRules()); 36 | 37 | var inotify = (INotifyPropertyChanged) bingo; 38 | 39 | var changed = new List(); 40 | 41 | inotify.PropertyChanged += (sender, args) => changed.Add(args.PropertyName); 42 | 43 | bingo.X = 1; 44 | 45 | Assert.AreEqual("BINGO", bingo.Message); 46 | Assert.AreEqual(101, instance.X); 47 | Assert.AreEqual(3, changed.Count); 48 | 49 | bingo.Message = "BONGO"; 50 | Assert.AreEqual("BONGO", bingo.Message); 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /EngineTests/CompositeObjectsWithInterfacesTestFixture.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using EngineTests.TestModelWithInterfaces; 3 | using NUnit.Framework; 4 | using RulesEngine.Interceptors; 5 | 6 | namespace EngineTests 7 | { 8 | [TestFixture] 9 | public class CompositeObjectsWithInterfacesTestFixture 10 | { 11 | 12 | [Test] 13 | public void Multi_threaded_test() 14 | { 15 | Parallel.For(0, 1000, (i) => Counterparty_change_fills_product()); 16 | } 17 | 18 | [Test] 19 | public void Counterparty_change_fills_product() 20 | { 21 | var trade = new CdsTrade 22 | { 23 | Product = new CreditDefaultSwap() 24 | }; 25 | 26 | var p = new InterfaceWrapper(trade, new CdsRules()).Target; 27 | 28 | p.CdsProduct.RefEntity = "AXA"; 29 | 30 | p.Counterparty = "CHASEOTC"; 31 | 32 | Assert.AreEqual("ICEURO", trade.ClearingHouse); 33 | Assert.AreEqual("MMR", trade.CdsProduct.Restructuring); 34 | Assert.AreEqual("SNR", trade.CdsProduct.Seniority); 35 | 36 | } 37 | 38 | [Test] 39 | public void Counterparty_change_fills_product2() 40 | { 41 | var trade = new CdsTrade2 42 | { 43 | Product = new CreditDefaultSwap2() 44 | }; 45 | 46 | var p = new InterfaceWrapper(trade, new CdsRules2()).Target; 47 | 48 | p.CdsProduct.RefEntity = "AXA"; 49 | 50 | p.Counterparty = "CHASEOTC"; 51 | 52 | Assert.AreEqual("ICEURO", trade.ClearingHouse); 53 | Assert.AreEqual("MMR", trade.CdsProduct.Restructuring); 54 | Assert.AreEqual("SNR", trade.CdsProduct.Seniority); 55 | 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /RulesEngine/RulesEngine/Rule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace RulesEngine.RulesEngine 6 | { 7 | /// 8 | /// A rule assigns a value to a single property. It can be triggered by a list of properties 9 | /// 10 | /// 11 | public class Rule 12 | { 13 | public ISet TriggerProperties { get; set; } = new HashSet(); 14 | 15 | 16 | /// 17 | /// Name of the property that will be updated by this rule 18 | /// 19 | public string TargetPropertyName { get; set; } 20 | 21 | /// 22 | /// Compiled method which updates the target property. It return true if value changed 23 | /// 24 | public Func Updater { get; set; } 25 | 26 | public string IfExplained { get; set; } 27 | 28 | public string ValueComputerExplained { get; set; } 29 | 30 | #region Overrides of Object 31 | 32 | /// 33 | /// Returns a string that represents the current object. 34 | /// 35 | /// 36 | /// A string that represents the current object. 37 | /// 38 | public override string ToString() 39 | { 40 | var builder = new StringBuilder(); 41 | 42 | builder.Append(TargetPropertyName); 43 | builder.Append(" = "); 44 | builder.Append(ValueComputerExplained); 45 | 46 | if (!string.IsNullOrWhiteSpace(IfExplained)) 47 | { 48 | builder.Append("\tIF "); 49 | builder.Append(IfExplained); 50 | 51 | } 52 | 53 | return builder.ToString(); 54 | 55 | 56 | } 57 | 58 | #endregion 59 | } 60 | } -------------------------------------------------------------------------------- /BusinessRulesEngine.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29418.71 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{41024DC3-013A-4F4D-9C95-62D9BFA3D21E}" 7 | ProjectSection(SolutionItems) = preProject 8 | README.md = README.md 9 | EndProjectSection 10 | EndProject 11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RulesEngine", "RulesEngine\RulesEngine.csproj", "{D45E8F2E-F206-40B5-B15C-4D2D558969CF}" 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EngineTests", "EngineTests\EngineTests.csproj", "{608454EB-29E7-4CE5-A829-F5B5B1A99FBB}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {D45E8F2E-F206-40B5-B15C-4D2D558969CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {D45E8F2E-F206-40B5-B15C-4D2D558969CF}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {D45E8F2E-F206-40B5-B15C-4D2D558969CF}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {D45E8F2E-F206-40B5-B15C-4D2D558969CF}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {608454EB-29E7-4CE5-A829-F5B5B1A99FBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {608454EB-29E7-4CE5-A829-F5B5B1A99FBB}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {608454EB-29E7-4CE5-A829-F5B5B1A99FBB}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {608454EB-29E7-4CE5-A829-F5B5B1A99FBB}.Release|Any CPU.Build.0 = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(SolutionProperties) = preSolution 31 | HideSolutionNode = FALSE 32 | EndGlobalSection 33 | GlobalSection(ExtensibilityGlobals) = postSolution 34 | SolutionGuid = {5671079C-E4FD-4953-9020-2F6DD442B523} 35 | EndGlobalSection 36 | EndGlobal 37 | -------------------------------------------------------------------------------- /RulesEngine/Tools/RuntimeCompiler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | 6 | namespace RulesEngine.Tools 7 | { 8 | /// 9 | /// Caches compiled versions of expression trees 10 | /// 11 | /// 12 | /// 13 | public static class RuntimeCompiler 14 | { 15 | private static readonly Dictionary> PrecompiledActions = 16 | new Dictionary>(); 17 | 18 | private static readonly Dictionary> PrecompiledGetters = 19 | new Dictionary>(); 20 | 21 | public static Action SetterFromGetter( 22 | Expression> expression) 23 | { 24 | var propertyName = ExpressionTreeHelper.FullPropertyName(expression); 25 | 26 | lock (PrecompiledActions) 27 | { 28 | if (PrecompiledActions.TryGetValue(propertyName, out var setter)) 29 | { 30 | return setter; 31 | } 32 | 33 | var valueParameterExpression = Expression.Parameter(typeof (TProperty)); 34 | var targetExpression = expression.Body is UnaryExpression unaryExpression 35 | ? unaryExpression.Operand 36 | : expression.Body; 37 | 38 | var assign = Expression.Lambda>( 39 | Expression.Assign(targetExpression, Expression.Convert(valueParameterExpression, targetExpression.Type)), 40 | expression.Parameters.Single(), valueParameterExpression 41 | ); 42 | 43 | return PrecompiledActions[propertyName] = assign.Compile(); 44 | } 45 | } 46 | 47 | public static Func Getter(Expression> expression) 48 | { 49 | var propertyName = ExpressionTreeHelper.FullPropertyName(expression); 50 | 51 | lock (PrecompiledGetters) 52 | { 53 | if (PrecompiledGetters.TryGetValue(propertyName, out var getter)) 54 | { 55 | return getter; 56 | } 57 | 58 | return PrecompiledGetters[propertyName] = expression.Compile(); 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /RulesEngine/Tools/ExpressionTreeHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Text; 5 | 6 | namespace RulesEngine.Tools 7 | { 8 | /// 9 | /// Common expression tree manipulation logic 10 | /// 11 | public static class ExpressionTreeHelper 12 | { 13 | /// 14 | /// Get the name of the most specific property expressed as an expression tree 15 | /// For example t=>t.Product.PremiumLeg.Coupon return "Coupon" 16 | /// 17 | /// 18 | /// 19 | /// 20 | /// 21 | public static string PropertyName(Expression> propertySelector) 22 | { 23 | if (propertySelector == null) throw new ArgumentNullException(nameof(propertySelector)); 24 | 25 | if (propertySelector.Body.NodeType == ExpressionType.Convert) 26 | { 27 | if (propertySelector.Body is UnaryExpression convert) 28 | if (convert.Operand is MemberExpression memberExpression) 29 | return memberExpression.Member.Name; 30 | } 31 | else 32 | { 33 | if (propertySelector.Body is MemberExpression memberExpression) return memberExpression.Member.Name; 34 | } 35 | 36 | throw new ArgumentException("propertySelector must be a MemberExpression.", nameof(propertySelector)); 37 | } 38 | 39 | /// 40 | /// Get the full name of the most specific property expressed as an expression tree 41 | /// For example t=>t.Product.PremiumLeg.Coupon return "Product.PremiumLeg.Coupon" 42 | /// 43 | /// 44 | /// 45 | /// 46 | /// 47 | public static string FullPropertyName(Expression> propertySelector) 48 | { 49 | if (propertySelector == null) throw new ArgumentNullException(nameof(propertySelector)); 50 | 51 | var memberExpression = propertySelector.Body as MemberExpression; 52 | 53 | if (memberExpression == null) 54 | throw new ArgumentException("propertySelector must be a MemberExpression.", nameof(propertySelector)); 55 | 56 | var components = new List 57 | { 58 | memberExpression.Member.Name 59 | }; 60 | var inner = memberExpression.Expression as MemberExpression; 61 | while (inner != null) 62 | { 63 | components.Add(inner.Member.Name); 64 | inner = inner.Expression as MemberExpression; 65 | } 66 | 67 | var sb = new StringBuilder(); 68 | components.Reverse(); 69 | 70 | foreach (var component in components) 71 | sb.Append(component) 72 | .Append("."); 73 | 74 | return sb.ToString() 75 | .TrimEnd('.'); 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /EngineTests/InterfaceWrapperTestFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Diagnostics; 5 | using NUnit.Framework; 6 | using RulesEngine.Interceptors; 7 | 8 | namespace EngineTests 9 | { 10 | [TestFixture] 11 | public class InterfaceWrapperTestFixture 12 | { 13 | [Test] 14 | public void Intercept_changes_with_interface_wrapper() 15 | { 16 | { 17 | var instance = new Abcd(); 18 | 19 | var rules = new AbcdRules(); 20 | 21 | Assert.AreEqual(4, rules.RulesCount); 22 | 23 | var abcd = new InterfaceWrapper(instance, rules); 24 | 25 | var inotify = (INotifyPropertyChanged) abcd; 26 | 27 | var changed = new List(); 28 | 29 | inotify.PropertyChanged += (sender, args) => changed.Add(args.PropertyName); 30 | 31 | abcd.Target.A = 1; 32 | 33 | Assert.AreEqual(100, abcd.Target.A); 34 | Assert.AreEqual(100, instance.A); 35 | Assert.AreEqual(4, changed.Count); 36 | } 37 | 38 | { 39 | var instance = new Bingo(); 40 | 41 | var bingo = new InterfaceWrapper(instance, new BingoRules()); 42 | 43 | var inotify = (INotifyPropertyChanged) bingo; 44 | 45 | var changed = new List(); 46 | 47 | inotify.PropertyChanged += (sender, args) => changed.Add(args.PropertyName); 48 | 49 | bingo.Target.X = 1; 50 | 51 | Assert.AreEqual("BINGO", bingo.Target.Message); 52 | Assert.AreEqual(101, instance.X); 53 | Assert.AreEqual(3, changed.Count); 54 | 55 | bingo.Target.Message = "BONGO"; 56 | Assert.AreEqual("BONGO", bingo.Target.Message); 57 | } 58 | } 59 | 60 | 61 | [Test] 62 | public void Intercept_changes_with_interface_wrapper_without_interface() 63 | { 64 | { 65 | var instance = new Xyz(); 66 | 67 | var xyz = new InterfaceWrapper(instance, new XyzRules()); 68 | 69 | xyz.Target.X = 1; 70 | 71 | Assert.AreEqual(2, instance.Y); 72 | 73 | Assert.AreEqual(4, instance.Z); 74 | 75 | } 76 | } 77 | 78 | 79 | [Test] 80 | public void Performance_test() 81 | { 82 | var rules = new XyzRules(); 83 | 84 | // warm up 85 | { 86 | var instance = new Xyz(); 87 | 88 | var xyz = new InterfaceWrapper(instance, rules); 89 | 90 | xyz.Target.X = 1; 91 | 92 | } 93 | 94 | var sw = new Stopwatch(); 95 | 96 | sw.Start(); 97 | for (int i = 0; i < 1000; i++) 98 | { 99 | var instance = new Xyz(); 100 | 101 | var xyz = new InterfaceWrapper(instance, rules); 102 | 103 | xyz.Target.X = 1; 104 | 105 | } 106 | 107 | sw.Stop(); 108 | 109 | Console.WriteLine($"took {sw.ElapsedMilliseconds} ms"); 110 | } 111 | } 112 | 113 | } -------------------------------------------------------------------------------- /EngineTests/TestModelWithoutInterfaces/CdsRules.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using RulesEngine.RulesEngine; 3 | 4 | namespace EngineTests.TestModelWithoutInterfaces 5 | { 6 | public class CdsRules : MappingRules 7 | { 8 | public CdsRules() 9 | { 10 | Set(t => t.CounterpartyRole) 11 | .With(t => t.Sales != null ? "Client" : "Dealer") 12 | .OnChanged(t => t.Sales); 13 | 14 | 15 | Set(t => t.ClearingHouse) 16 | .With(t => GetDefaultClearingHouse(t.Counterparty, t.CdsProduct.RefEntity)) 17 | .OnChanged(t => t.CdsProduct.RefEntity, t=>t.Counterparty); 18 | 19 | 20 | Set(t => t.SalesCredit) 21 | .With(t => Calculator(t.CdsProduct.Spread, t.CdsProduct.Nominal)) 22 | .OnChanged(t => t.CdsProduct.Spread, t=> t.CdsProduct); 23 | 24 | Set(t => t.CdsProduct.TransactionType) 25 | .With(t => GetTransactionType(t.CdsProduct.RefEntity)) 26 | .OnChanged(t => t.CdsProduct.RefEntity); 27 | 28 | 29 | Set(t => t.CdsProduct.Currency) 30 | .With(t => GetDefaultCurrency(t.CdsProduct.TransactionType)) 31 | .OnChanged(t => t.CdsProduct.TransactionType); 32 | 33 | 34 | Set(t => t.CdsProduct.Restructuring) 35 | .With(t => GetDefaultRestructuring(t.CdsProduct.TransactionType)) 36 | .OnChanged(t => t.CdsProduct.TransactionType); 37 | 38 | 39 | Set(t => t.CdsProduct.Seniority) 40 | .With(t => GetDefaultSeniority(t.CdsProduct.TransactionType)) 41 | .OnChanged(t => t.CdsProduct.TransactionType); 42 | 43 | } 44 | 45 | private string GetTransactionType(string refEntity) 46 | { 47 | if (refEntity == "AXA") 48 | { 49 | return "Standard European Corporate"; 50 | } 51 | 52 | return null; 53 | } 54 | 55 | private string GetDefaultSeniority(string transactionType) 56 | { 57 | if (transactionType == "Standard European Corporate") 58 | { 59 | return "SNR"; 60 | } 61 | 62 | return null; 63 | } 64 | 65 | private string GetDefaultRestructuring(string transactionType) 66 | { 67 | if (transactionType == "Standard European Corporate") 68 | { 69 | return "MMR"; 70 | } 71 | 72 | return null; 73 | } 74 | 75 | private string GetDefaultCurrency(string transactionType) 76 | { 77 | if (transactionType == "Standard European Corporate") 78 | { 79 | return "EUR"; 80 | } 81 | 82 | return null; 83 | } 84 | 85 | private static decimal Calculator(decimal spread, decimal nominal) 86 | { 87 | return 4; 88 | } 89 | 90 | private static string GetDefaultClearingHouse(string counterpary, string refEntity) 91 | { 92 | if (refEntity == "AXA" && counterpary == "CHASEOTC") 93 | { 94 | return "ICEURO"; 95 | } 96 | 97 | if (refEntity == "RENAULT" && counterpary == "CHASEOTC") 98 | { 99 | return "ICETRUST"; 100 | } 101 | 102 | return null; 103 | } 104 | 105 | 106 | protected override void Trace(Rule triggeredRule, string triggerProperty, CdsTrade instance) 107 | { 108 | Console.WriteLine(triggeredRule); 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /EngineTests/TestModelWithInterfaces/CdsRules2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using RulesEngine.RulesEngine; 3 | 4 | namespace EngineTests.TestModelWithInterfaces 5 | { 6 | public class CdsRules2 : MappingRules 7 | { 8 | public CdsRules2() 9 | { 10 | Set(t => t.CounterpartyRole) 11 | .With(t => t.Sales != null ? "Client" : "Dealer") 12 | .OnChanged(t => t.Sales); 13 | 14 | 15 | Set(t => t.ClearingHouse) 16 | .With(t => GetDefaultClearingHouse(t.Counterparty, t.CdsProduct.RefEntity)) 17 | .OnChanged(t => t.CdsProduct.RefEntity, t => t.Counterparty); 18 | 19 | 20 | Set(t => t.SalesCredit) 21 | .With(t => Calculator(t.CdsProduct.Spread, t.CdsProduct.Nominal)) 22 | .OnChanged(t => t.CdsProduct.Spread, t => t.CdsProduct.RefEntity); 23 | 24 | 25 | Set(t => t.CdsProduct.TransactionType) 26 | .With(t => GetTransactionType(t.CdsProduct.RefEntity)) 27 | .OnChanged(t => t.CdsProduct.RefEntity); 28 | 29 | 30 | Set(t => t.CdsProduct.Currency) 31 | .With(t => GetDefaultCurrency(t.CdsProduct.TransactionType)) 32 | .OnChanged(t => t.CdsProduct.TransactionType); 33 | 34 | 35 | Set(t => t.CdsProduct.Restructuring) 36 | .With(t => GetDefaultRestructuring(t.CdsProduct.TransactionType)) 37 | .OnChanged(t => t.CdsProduct.TransactionType); 38 | 39 | 40 | Set(t => t.CdsProduct.Seniority) 41 | .With(t => GetDefaultSeniority(t.CdsProduct.TransactionType)) 42 | .OnChanged(t => t.CdsProduct.TransactionType); 43 | 44 | } 45 | 46 | private string GetTransactionType(string refEntity) 47 | { 48 | if (refEntity == "AXA") 49 | { 50 | return "Standard European Corporate"; 51 | } 52 | 53 | return null; 54 | } 55 | 56 | private string GetDefaultSeniority(string transactionType) 57 | { 58 | if (transactionType == "Standard European Corporate") 59 | { 60 | return "SNR"; 61 | } 62 | 63 | return null; 64 | } 65 | 66 | private string GetDefaultRestructuring(string transactionType) 67 | { 68 | if (transactionType == "Standard European Corporate") 69 | { 70 | return "MMR"; 71 | } 72 | 73 | return null; 74 | } 75 | 76 | private string GetDefaultCurrency(string transactionType) 77 | { 78 | if (transactionType == "Standard European Corporate") 79 | { 80 | return "EUR"; 81 | } 82 | 83 | return null; 84 | } 85 | 86 | private static decimal Calculator(decimal spread, decimal nominal) 87 | { 88 | return 4; 89 | } 90 | 91 | private static string GetDefaultClearingHouse(string counterpary, string refEntity) 92 | { 93 | if (refEntity == "AXA" && counterpary == "CHASEOTC") 94 | { 95 | return "ICEURO"; 96 | } 97 | 98 | if (refEntity == "RENAULT" && counterpary == "CHASEOTC") 99 | { 100 | return "ICETRUST"; 101 | } 102 | 103 | return null; 104 | } 105 | 106 | #region Overrides of MappingRules 107 | 108 | protected override void Trace(Rule triggeredRule, string triggerProperty, CdsTrade2 instance) 109 | { 110 | Console.WriteLine(triggeredRule); 111 | } 112 | 113 | #endregion 114 | } 115 | } -------------------------------------------------------------------------------- /RulesEngine/Interceptors/InterfaceWrapper.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using Castle.DynamicProxy; 3 | using RulesEngine.RulesEngine; 4 | 5 | namespace RulesEngine.Interceptors 6 | { 7 | /// 8 | /// Intercept calls to non sealed classes that have all public properties virtual 9 | /// Calls to setters are diverted to rule engine 10 | /// Calls to getters are intercepted to wrap the returned value (if it is an interface) 11 | /// Calls to methods are silently ignored 12 | /// 13 | /// 14 | public sealed class InterfaceWrapper : INotifyPropertyChanged 15 | where T : class 16 | { 17 | // ReSharper disable once StaticMemberInGenericType 18 | private static ProxyGenerator Generator { get; } = new ProxyGenerator(); 19 | 20 | /// 21 | /// Initializes a new instance of the class. 22 | /// 23 | public InterfaceWrapper(T instance, MappingRules rules) 24 | { 25 | Target = typeof(T).IsInterface ? Generator.CreateInterfaceProxyWithTarget(instance, new Interceptor(rules, this, instance)) : Generator.CreateClassProxyWithTarget(instance, new Interceptor(rules, this, instance)); 26 | } 27 | 28 | public T Target { get; } 29 | 30 | #region Implementation of INotifyPropertyChanged 31 | 32 | public event PropertyChangedEventHandler PropertyChanged; 33 | 34 | #endregion 35 | 36 | private void OnPropertyChanged(string propertyName = null) 37 | { 38 | var handler = PropertyChanged; 39 | handler?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 40 | } 41 | 42 | private class Interceptor : IInterceptor 43 | { 44 | private readonly InterfaceWrapper _parent; 45 | private readonly T _instance; 46 | private readonly MappingRules _rules; 47 | 48 | public Interceptor(MappingRules rules, InterfaceWrapper parent, T instance) 49 | { 50 | _rules = rules; 51 | _parent = parent; 52 | _instance = instance; 53 | } 54 | 55 | public void Intercept(IInvocation invocation) 56 | { 57 | var methodName = invocation.Method.Name; 58 | if (methodName.StartsWith("get_")) 59 | { 60 | invocation.Proceed(); 61 | 62 | var getterReturnType = invocation.MethodInvocationTarget.ReturnType; 63 | 64 | if (getterReturnType.IsInterface) 65 | { 66 | // wrap the result of the getter in a proxy 67 | var proxy = Generator.CreateInterfaceProxyWithTarget(getterReturnType, invocation.ReturnValue, 68 | new Interceptor(_rules, _parent, _instance)); 69 | 70 | invocation.ReturnValue = proxy; 71 | } 72 | else if(getterReturnType.IsClass && !getterReturnType.Namespace.StartsWith("System")) // avoid elementary types that are class 73 | { 74 | var proxy = Generator.CreateClassProxyWithTarget(getterReturnType, invocation.ReturnValue, 75 | new Interceptor(_rules, _parent, _instance)); 76 | 77 | invocation.ReturnValue = proxy; 78 | } 79 | } 80 | else if (methodName.StartsWith("set_")) 81 | { 82 | var propertyName = methodName.Substring(4); 83 | 84 | var modified = _rules.SetProperty(propertyName, _instance, invocation.InvocationTarget, invocation.Arguments[0]); 85 | 86 | foreach (var property in modified) 87 | { 88 | _parent.OnPropertyChanged(property); 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /RulesEngine/RulesEngine/Explain/Explain.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Linq.Expressions; 4 | using System.Runtime.InteropServices; 5 | using Remotion.Linq; 6 | using Remotion.Linq.Clauses; 7 | 8 | namespace RulesEngine.RulesEngine.Explain 9 | { 10 | public static class Explain 11 | { 12 | public static string TryExplain(this Expression> ifExpression) 13 | { 14 | //create a fake queryable to force query parsing and capture resolution 15 | 16 | var executor = new NullExecutor(); 17 | var queryable = new NullQueryable(executor); 18 | 19 | try 20 | { 21 | var unused = queryable.Where(ifExpression).ToList(); 22 | 23 | return executor.Expression.ToString(); 24 | } 25 | catch 26 | { 27 | // expression too complex 28 | return null; 29 | } 30 | 31 | } 32 | 33 | public static string TryExplain(this Expression> expression) 34 | { 35 | // create a fake queryable to force query parsing and capture resolution 36 | 37 | return ExplainSimple(expression.Body) ?? ExplainBinaryExpression(expression.Body) ?? "?"; 38 | 39 | } 40 | 41 | private static string ExplainSimple(Expression expression) 42 | { 43 | if (expression is MemberExpression member) 44 | { 45 | return member.Member.Name; 46 | } 47 | 48 | if (expression is MethodCallExpression call) 49 | { 50 | return call.Method.Name + "()"; 51 | } 52 | 53 | if (expression is ConstantExpression constant) 54 | { 55 | return constant.Value.ToString(); 56 | } 57 | 58 | return null; 59 | } 60 | 61 | 62 | 63 | private static string ExplainBinaryExpression(Expression expr) 64 | { 65 | if (expr is BinaryExpression expression) 66 | { 67 | string left = ExplainSimple(expression.Left) ?? ExplainBinaryExpression(expression.Left) ??"?"; 68 | string right = ExplainSimple(expression.Right) ?? ExplainBinaryExpression(expression.Right) ?? "?"; 69 | 70 | if (expression.NodeType == ExpressionType.Add) 71 | return left + " + " +right; 72 | 73 | if (expression.NodeType == ExpressionType.Subtract) 74 | return left + " - " + right; 75 | 76 | if (expression.NodeType == ExpressionType.Multiply) 77 | return left + " * " + right; 78 | 79 | if (expression.NodeType == ExpressionType.Divide) 80 | return left + " / " + right; 81 | 82 | if (expression.NodeType == ExpressionType.LessThan) 83 | return left + " < " + right; 84 | 85 | if (expression.NodeType == ExpressionType.LessThanOrEqual) 86 | return left + " <= " + right; 87 | 88 | if (expression.NodeType == ExpressionType.GreaterThan) 89 | return left + " > " + right; 90 | 91 | if (expression.NodeType == ExpressionType.GreaterThanOrEqual) 92 | return left + " >= " + right; 93 | 94 | if (expression.NodeType == ExpressionType.Equal) 95 | return left + " = " + right; 96 | 97 | if (expression.NodeType == ExpressionType.NotEqual) 98 | return left + " <> " + right; 99 | 100 | if (expression.NodeType == ExpressionType.AndAlso) 101 | return left + " AND " + right; 102 | 103 | if (expression.NodeType == ExpressionType.Or) 104 | return left + " OR " + right; 105 | 106 | 107 | } 108 | 109 | return null; 110 | } 111 | 112 | } 113 | } -------------------------------------------------------------------------------- /EngineTests/TestModelWithInterfaces/CdsRules.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using RulesEngine.RulesEngine; 3 | 4 | namespace EngineTests.TestModelWithInterfaces 5 | { 6 | public class CdsRules : MappingRules 7 | { 8 | public CdsRules() 9 | { 10 | 11 | 12 | Set(t => t.CounterpartyRole) 13 | .With(t => "Client") 14 | .If(t=>t.Sales != null) 15 | .OnChanged(t => t.Sales); 16 | 17 | Set(t => t.CounterpartyRole) 18 | .With(t => "Dealer") 19 | .If(t => t.Sales == null) 20 | .OnChanged(t => t.Sales); 21 | 22 | 23 | Set(t => t.ClearingHouse) 24 | .With(t => GetDefaultClearingHouse(t.Counterparty, t.CdsProduct.RefEntity)) 25 | .OnChanged(t => t.CdsProduct.RefEntity, t => t.Counterparty); 26 | 27 | 28 | Set(t => t.SalesCredit) 29 | .With(t => Calculator(t.CdsProduct.Spread, t.CdsProduct.Nominal)) 30 | .OnChanged(t => t.CdsProduct.Spread, t => t.CdsProduct.RefEntity); 31 | 32 | 33 | Set(t => t.CdsProduct.TransactionType) 34 | .With(t => GetTransactionType(t.CdsProduct.RefEntity)) 35 | .OnChanged(t => t.CdsProduct.RefEntity); 36 | 37 | 38 | Set(t => t.CdsProduct.Currency) 39 | .With(t => GetDefaultCurrency(t.CdsProduct.TransactionType)) 40 | .OnChanged(t => t.CdsProduct.TransactionType); 41 | 42 | 43 | Set(t => t.CdsProduct.Restructuring) 44 | .With(t => GetDefaultRestructuring(t.CdsProduct.TransactionType)) 45 | .OnChanged(t => t.CdsProduct.TransactionType); 46 | 47 | 48 | Set(t => t.CdsProduct.Seniority) 49 | .With(t => GetDefaultSeniority(t.CdsProduct.TransactionType)) 50 | .OnChanged(t => t.CdsProduct.TransactionType); 51 | 52 | } 53 | 54 | private string GetTransactionType(string refEntity) 55 | { 56 | if (refEntity == "AXA") 57 | { 58 | return "Standard European Corporate"; 59 | } 60 | 61 | return null; 62 | } 63 | 64 | private string GetDefaultSeniority(string transactionType) 65 | { 66 | if (transactionType == "Standard European Corporate") 67 | { 68 | return "SNR"; 69 | } 70 | 71 | return null; 72 | } 73 | 74 | private string GetDefaultRestructuring(string transactionType) 75 | { 76 | if (transactionType == "Standard European Corporate") 77 | { 78 | return "MMR"; 79 | } 80 | 81 | return null; 82 | } 83 | 84 | private string GetDefaultCurrency(string transactionType) 85 | { 86 | if (transactionType == "Standard European Corporate") 87 | { 88 | return "EUR"; 89 | } 90 | 91 | return null; 92 | } 93 | 94 | private static decimal Calculator(decimal spread, decimal nominal) 95 | { 96 | return 4; 97 | } 98 | 99 | private static string GetDefaultClearingHouse(string counterpary, string refEntity) 100 | { 101 | if (refEntity == "AXA" && counterpary == "CHASEOTC") 102 | { 103 | return "ICEURO"; 104 | } 105 | 106 | if (refEntity == "RENAULT" && counterpary == "CHASEOTC") 107 | { 108 | return "ICETRUST"; 109 | } 110 | 111 | return null; 112 | } 113 | 114 | #region Overrides of MappingRules 115 | 116 | protected override void Trace(Rule triggeredRule, string triggerProperty, ICdsTrade instance) 117 | { 118 | Console.WriteLine(triggeredRule); 119 | } 120 | 121 | #endregion 122 | } 123 | } -------------------------------------------------------------------------------- /RulesEngine/Interceptors/DynamicWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Dynamic; 4 | using RulesEngine.RulesEngine; 5 | using RulesEngine.Tools; 6 | 7 | namespace RulesEngine.Interceptors 8 | { 9 | /// 10 | /// Wraps business object as a . It can be used tu intercept accessor calls on types that 11 | /// can not be wrapped in typed proxies (for example sealed classes) 12 | /// 13 | /// 14 | public sealed class DynamicWrapper : DynamicObject, INotifyPropertyChanged 15 | { 16 | /// 17 | /// The rules engine 18 | /// 19 | private readonly MappingRules _businessRules; 20 | 21 | private readonly object _root; 22 | private readonly object _wrappedObject; 23 | 24 | 25 | internal DynamicWrapper(object root, object obj, MappingRules businessRules) 26 | { 27 | _root = root; 28 | _wrappedObject = obj ?? throw new ArgumentNullException(nameof(obj)); 29 | _businessRules = businessRules; 30 | } 31 | 32 | 33 | public DynamicWrapper(object root, MappingRules businessRules) 34 | { 35 | 36 | _wrappedObject = root ?? throw new ArgumentNullException(nameof(root)); 37 | _root = root; 38 | _businessRules = businessRules; 39 | } 40 | 41 | #region Implementation of INotifyPropertyChanged 42 | 43 | public event PropertyChangedEventHandler PropertyChanged; 44 | 45 | #endregion 46 | 47 | private void OnPropertyChanged(string propertyName = null) 48 | { 49 | var handler = PropertyChanged; 50 | handler?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 51 | } 52 | 53 | #region Overrides of DynamicObject 54 | 55 | /// 56 | /// For the moment let the method calls pass through. 57 | /// 58 | /// 59 | /// 60 | /// 61 | /// 62 | public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) 63 | { 64 | try 65 | { 66 | //call _wrappedObject object 67 | result = _wrappedObject.GetType() 68 | .GetMethod(binder.Name) 69 | ?.Invoke(_wrappedObject, args); 70 | 71 | return true; 72 | } 73 | 74 | catch 75 | { 76 | result = null; 77 | return false; 78 | } 79 | } 80 | 81 | /// 82 | /// Intercept calls to the property setters and divert them to the rules engine 83 | /// 84 | /// 85 | /// 86 | /// 87 | public override bool TrySetMember(SetMemberBinder binder, object value) 88 | { 89 | try 90 | { 91 | var modified = _businessRules.SetProperty(binder.Name, _root, _wrappedObject, value); 92 | 93 | foreach (var property in modified) OnPropertyChanged(property); 94 | 95 | return true; 96 | } 97 | catch 98 | { 99 | return false; 100 | } 101 | } 102 | 103 | 104 | private static bool IsComplexType(Type type) 105 | { 106 | if (type.IsValueType) return false; 107 | 108 | if (type == typeof(string)) return false; 109 | 110 | return type.IsClass; 111 | } 112 | 113 | /// 114 | /// Intercept calls to property getters. Let the invocation being done normally for now 115 | /// 116 | /// 117 | /// 118 | /// 119 | public override bool TryGetMember(GetMemberBinder binder, out object result) 120 | { 121 | try 122 | { 123 | var getter = CompiledAccessors.CompiledGetter(_wrappedObject.GetType(), binder.Name); 124 | var getterResult = getter(_wrappedObject); 125 | 126 | result = IsComplexType(getterResult.GetType()) 127 | ? new DynamicWrapper(_root, getterResult, _businessRules) 128 | : getterResult; 129 | 130 | return true; 131 | } 132 | catch 133 | { 134 | result = null; 135 | return false; 136 | } 137 | } 138 | 139 | #endregion 140 | } 141 | } -------------------------------------------------------------------------------- /RulesEngine/RulesEngine/Explain/LeafExpression.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Linq.Expressions; 4 | using System.Text; 5 | 6 | namespace RulesEngine.RulesEngine.Explain 7 | { 8 | /// 9 | /// The smallest expression we are able to interpret 10 | /// 11 | 12 | public sealed class LeafExpression : ExplainExpression 13 | { 14 | public string MemberName { get; set; } 15 | 16 | private HashSet _inValues = new HashSet(); 17 | 18 | /// 19 | /// Parameter-less constructor used for serialization 20 | /// 21 | public LeafExpression() 22 | { 23 | } 24 | 25 | 26 | /// 27 | /// Build a simple atomic query (one value and unary operator) 28 | /// 29 | /// 30 | /// 31 | /// 32 | public LeafExpression(string memberName, object value, QueryOperator @operator = QueryOperator.Eq) 33 | { 34 | MemberName = memberName; 35 | Value = value; 36 | Operator = @operator; 37 | } 38 | 39 | 40 | /// 41 | /// Build an IN query 42 | /// 43 | /// 44 | /// 45 | public LeafExpression(string memberName, IEnumerable values) 46 | { 47 | MemberName = memberName; 48 | _inValues = new HashSet(values); 49 | Operator = QueryOperator.In; 50 | } 51 | 52 | 53 | /// 54 | /// Check if the query is valid 55 | /// 56 | public override bool IsValid 57 | { 58 | get 59 | { 60 | // IN requires a list of values 61 | if (Operator == QueryOperator.In && InValues.Count == 0) 62 | return false; 63 | 64 | // only IN accepts a list of values 65 | if (Operator != QueryOperator.In && InValues.Count > 0) 66 | return false; 67 | 68 | // any operator except IN requires at least a value 69 | if (Operator != QueryOperator.In && ReferenceEquals(Value, null)) 70 | return false; 71 | 72 | return true; 73 | } 74 | } 75 | 76 | 77 | public object Value { get; } 78 | 79 | /// 80 | /// used for binary operators 81 | /// 82 | 83 | /// 84 | /// The operator of the atomic query 85 | /// 86 | public QueryOperator Operator { get; set; } 87 | 88 | 89 | public ICollection InValues 90 | { 91 | get => _inValues; 92 | set => _inValues = new HashSet(value); 93 | } 94 | 95 | public IList Values => _inValues.Count > 0 96 | ? _inValues.ToList(): new List {Value}; 97 | 98 | public string MethodCall { get; set; } 99 | 100 | 101 | public override string ToString() 102 | { 103 | 104 | if (MethodCall != null) 105 | return MethodCall; 106 | 107 | var result = new StringBuilder(); 108 | 109 | result.Append(MemberName); 110 | 111 | switch (Operator) 112 | { 113 | case QueryOperator.Eq: 114 | result.Append( " = "); 115 | break; 116 | case QueryOperator.NotEqual: 117 | result.Append(" <> "); 118 | break; 119 | case QueryOperator.Le: 120 | result.Append(" <= "); 121 | break; 122 | case QueryOperator.Lt: 123 | result.Append(" < "); 124 | break; 125 | case QueryOperator.Gt: 126 | result.Append(" > "); 127 | break; 128 | case QueryOperator.Ge: 129 | result.Append(" >= "); 130 | break; 131 | case QueryOperator.In: 132 | result.Append(" IN "); 133 | break; 134 | } 135 | 136 | 137 | if (Operator == QueryOperator.In) 138 | { 139 | result.Append("("); 140 | 141 | result.Append(string.Join(", ", InValues.ToArray())); 142 | 143 | result.Append(")"); 144 | 145 | } 146 | else 147 | { 148 | result.Append(Value ?? "null"); 149 | } 150 | 151 | return result.ToString(); 152 | } 153 | 154 | } 155 | } -------------------------------------------------------------------------------- /EngineTests/RulesEngineTestFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using EngineTests.TestModelWithoutInterfaces; 5 | using NUnit.Framework; 6 | using RulesEngine.Interceptors; 7 | 8 | namespace EngineTests 9 | { 10 | [TestFixture] 11 | public class RulesEngineTestFixture 12 | { 13 | /// 14 | /// Runs en action multiple times and count the time 15 | /// 16 | /// 17 | /// 18 | /// 19 | private long TimeInMilliseconds(Action action, int runs) 20 | { 21 | // run once without counter to force JIT 22 | action(0); 23 | var stopWatch = new Stopwatch(); 24 | 25 | stopWatch.Start(); 26 | 27 | try 28 | { 29 | for (var i = 0; i < runs; i++) 30 | { 31 | action(i); 32 | } 33 | } 34 | finally 35 | { 36 | stopWatch.Stop(); 37 | } 38 | 39 | return stopWatch.ElapsedMilliseconds; 40 | } 41 | 42 | [Test] 43 | public void Cascading_perf_test() 44 | { 45 | var abcd = new Abcd(); 46 | var abcdRules = new AbcdRules(); 47 | 48 | var time0 = TimeInMilliseconds(i => 49 | { 50 | abcdRules.SetProperty("A", abcd, abcd, 1); 51 | Assert.AreEqual(100, abcd.A); 52 | }, 100); 53 | 54 | Console.WriteLine("Cascade rules {0} ms", time0); 55 | 56 | Assert.Less(time0, 100); 57 | } 58 | 59 | [Test] 60 | public void Check_cascading_by_rule_engine() 61 | { 62 | var bingo = new Bingo(); 63 | var rules = new BingoRules(); 64 | 65 | { 66 | // setting Message property does not trigger any rule 67 | var modified = rules.SetProperty("Message", bingo, bingo, "test"); 68 | Assert.AreEqual(1, modified.Count); 69 | Assert.IsTrue(modified.Contains("Message")); 70 | Assert.AreEqual("test", bingo.Message); 71 | } 72 | 73 | { 74 | var modified = rules.SetProperty("X", bingo, bingo, 3); 75 | 76 | Assert.AreEqual(3, modified.Count); 77 | Assert.IsTrue(modified.Contains("X")); 78 | Assert.IsTrue(modified.Contains("Y")); 79 | Assert.IsTrue(modified.Contains("Message")); 80 | 81 | Assert.AreEqual("BINGO", bingo.Message); 82 | 83 | var x = bingo.X; 84 | 85 | // setting the same value should not trigger any rule 86 | modified = rules.SetProperty("X", bingo, bingo, x); 87 | Assert.AreEqual(0, modified.Count); 88 | } 89 | 90 | { 91 | var abcd = new Abcd(); 92 | var abcdRules = new AbcdRules(); 93 | 94 | var modified = abcdRules.SetProperty("A", abcd, abcd, 1); 95 | Assert.AreEqual(4, modified.Count); 96 | Assert.IsTrue(modified.Contains("A")); 97 | Assert.IsTrue(modified.Contains("B")); 98 | Assert.IsTrue(modified.Contains("C")); 99 | Assert.IsTrue(modified.Contains("D")); 100 | Assert.AreEqual(100, abcd.A); 101 | } 102 | } 103 | 104 | 105 | 106 | [Test] 107 | public void Explicitly_trigger_a_rule_set() 108 | { 109 | var abcd = new Abcd(); 110 | var abcdRules = new AbcdRules(); 111 | 112 | var modified = abcdRules.TriggerAll(abcd); 113 | 114 | Assert.AreEqual(4, modified.Count()); 115 | Assert.IsTrue(modified.Contains("A")); 116 | Assert.IsTrue(modified.Contains("B")); 117 | Assert.IsTrue(modified.Contains("C")); 118 | Assert.IsTrue(modified.Contains("D")); 119 | Assert.AreEqual(100, abcd.A); 120 | 121 | var dog = new Dog {Age = 14, Name = "Max"}; 122 | var doggyRules = new DogRules(); 123 | 124 | doggyRules.TriggerAll(dog); 125 | Assert.AreEqual(true, dog.IsDangerous); 126 | Assert.AreEqual("ball", dog.FavoriteToy); 127 | 128 | var trade = new CdsTrade 129 | { 130 | Product = new CreditDefaultSwap { RefEntity = "AXA"}, 131 | Counterparty = "CHASEOTC" 132 | 133 | }; 134 | 135 | var cdsRules = new CdsRules(); 136 | 137 | cdsRules.TriggerAll(trade); 138 | 139 | 140 | Assert.AreEqual("ICEURO", trade.ClearingHouse); 141 | Assert.AreEqual("MMR", trade.CdsProduct.Restructuring); 142 | Assert.AreEqual("SNR", trade.CdsProduct.Seniority); 143 | 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /EngineTests/UtilitiesTestFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using EngineTests.TestModelWithoutInterfaces; 4 | using NUnit.Framework; 5 | using RulesEngine.Tools; 6 | 7 | namespace EngineTests 8 | { 9 | [TestFixture] 10 | public class UtilitiesTestFixture 11 | { 12 | /// 13 | /// Runs en action multiple times and count the time 14 | /// 15 | /// 16 | /// 17 | /// 18 | private long TimeInMilliseconds(Action action, int runs) 19 | { 20 | // run once without counter to force JIT 21 | action(0); 22 | var stopWatch = new Stopwatch(); 23 | 24 | stopWatch.Start(); 25 | 26 | try 27 | { 28 | for (var i = 0; i < runs; i++) 29 | { 30 | action(i); 31 | } 32 | } 33 | finally 34 | { 35 | stopWatch.Stop(); 36 | } 37 | 38 | return stopWatch.ElapsedMilliseconds; 39 | } 40 | 41 | [Test] 42 | public void Generate_setter_from_getter_expression() 43 | { 44 | var setter = RuntimeCompiler.SetterFromGetter(cds => cds.CdsProduct.RefEntity); 45 | 46 | var trade = new CdsTrade 47 | { 48 | Product = new CreditDefaultSwap() 49 | }; 50 | 51 | setter(trade, "xxx"); 52 | 53 | Assert.AreEqual("xxx", trade.CdsProduct.RefEntity); 54 | } 55 | 56 | [Test] 57 | public void Get_property_name_from_complex_expression() 58 | { 59 | var name = ExpressionTreeHelper.PropertyName((Bingo b) => b.Message.Length); 60 | Assert.AreEqual("Length", name); 61 | 62 | var fullName = ExpressionTreeHelper.FullPropertyName((Bingo b) => b.Message.Length); 63 | Assert.AreEqual("Message.Length", fullName); 64 | 65 | var fullName1 = ExpressionTreeHelper.FullPropertyName((Bingo b) => b.Message); 66 | Assert.AreEqual("Message", fullName1); 67 | 68 | var fullName2 = ExpressionTreeHelper.FullPropertyName((CdsTrade cds) => cds.CdsProduct.RefEntity.Length); 69 | Assert.AreEqual("CdsProduct.RefEntity.Length", fullName2); 70 | } 71 | 72 | [Test] 73 | public void Precompiled_accessors_are_much_faster_then_reflexion_based_ones() 74 | { 75 | var property = typeof (Bingo).GetProperty("X"); 76 | 77 | var getter = CompiledAccessors.CompiledGetter(typeof (Bingo), "X"); 78 | var setter = CompiledAccessors.CompiledSetter(typeof (Bingo), "X"); 79 | var smartSetter = CompiledAccessors.CompiledSmartSetter(typeof (Bingo), "X"); 80 | 81 | var bingo = new Bingo 82 | { 83 | X = 3, 84 | Y = 4 85 | }; 86 | 87 | var time0 = TimeInMilliseconds(i => 88 | { 89 | var before = (int) getter(bingo); 90 | setter(bingo, before + 1); 91 | }, 1000000); 92 | 93 | Console.WriteLine("Compiled took {0} ms", time0); 94 | 95 | var time1 = TimeInMilliseconds(i => 96 | { 97 | var before = (int) property.GetValue(bingo); 98 | property.SetValue(bingo, before + 1); 99 | }, 1000000); 100 | 101 | Console.WriteLine("Reflexion-based took {0} ms", time1); 102 | 103 | var time2 = TimeInMilliseconds(i => smartSetter(bingo, i), 1000000); 104 | Console.WriteLine("Compiled smart seter took {0} ms", time2); 105 | 106 | Assert.Less(time2*5, time1); // compiled smart setters should be faster 107 | Assert.Less(time0*5, time1); // compiled accessors should be much faster than reflexion based ones 108 | } 109 | 110 | [Test] 111 | public void Update_an_object_using_precompiled_accessors() 112 | { 113 | var bingo = new Bingo 114 | { 115 | X = 3, 116 | Y = 4 117 | }; 118 | 119 | var x = CompiledAccessors.CompiledGetter(typeof (Bingo), "X")(bingo); 120 | Assert.AreEqual(3, x); 121 | 122 | CompiledAccessors.CompiledSetter(typeof (Bingo), "X")(bingo, 4); 123 | x = CompiledAccessors.CompiledGetter(typeof (Bingo), "X")(bingo); 124 | Assert.AreEqual(4, x); 125 | 126 | var smartSetter = CompiledAccessors.CompiledSmartSetter(typeof (Bingo), "Message"); 127 | Assert.IsNotNull(smartSetter); 128 | 129 | // a smart setter sets the property only if value is different 130 | var changed = smartSetter(bingo, "test"); 131 | Assert.IsTrue(changed); 132 | 133 | changed = smartSetter(bingo, "test"); 134 | Assert.IsFalse(changed); 135 | 136 | changed = smartSetter(bingo, "different"); 137 | Assert.IsTrue(changed); 138 | } 139 | } 140 | } -------------------------------------------------------------------------------- /RulesEngine/RulesEngine/MappingRules.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using RulesEngine.Tools; 5 | 6 | namespace RulesEngine.RulesEngine 7 | { 8 | /// 9 | /// The business rules engine. It triggers the corresponding rule execution when a property changes from an external 10 | /// source 11 | /// or from a previous rule execution 12 | /// Concrete classes should inherit from this one and declare all the rules in the constructor by using a fluent syntax 13 | /// 14 | /// 15 | public abstract class MappingRules 16 | { 17 | private readonly Dictionary>> _rulesByTrigger = 18 | new Dictionary>>(); 19 | 20 | 21 | private readonly List> _rules = new List>(); 22 | 23 | /// 24 | /// If set triggers an exception which prevent a stack overflow if the specified level of recursion is over the 25 | /// threshold 26 | /// 27 | protected int RecursionLimit { get; set; } 28 | 29 | public IDictionary>> RulesByTrigger => _rulesByTrigger; 30 | 31 | public IList> Rules=> _rules; 32 | 33 | 34 | /// 35 | /// First declaration of the fluent syntax. Sets the target property of the rule 36 | /// 37 | /// 38 | /// 39 | /// 40 | protected FluentExtensions.FluentToken Set( 41 | Expression> propertySelector) 42 | { 43 | 44 | return new FluentExtensions.FluentToken 45 | { 46 | MappingRulesContainer = this, 47 | TargetPropertySelector = propertySelector 48 | }; 49 | 50 | } 51 | 52 | /// 53 | /// 54 | /// 55 | /// the root (entry point) of the object graph 56 | /// the owner of the property 57 | /// 58 | /// 59 | public ICollection SetProperty(string propertyName, object root, object parent, object value) 60 | { 61 | var modified = new HashSet(); 62 | 63 | var smartSetter = CompiledAccessors.CompiledSmartSetter(parent.GetType(), propertyName); 64 | 65 | var hasChanged = smartSetter(parent, value); 66 | if (hasChanged) 67 | { 68 | modified.Add(propertyName); 69 | Cascade(propertyName, root, modified, 1); 70 | } 71 | 72 | return modified; 73 | } 74 | 75 | private void Cascade(string propertyName, object root, HashSet modified, 76 | int recursionLimit) 77 | { 78 | if (RecursionLimit > 0) 79 | if (recursionLimit > RecursionLimit) 80 | throw new NotSupportedException("Recursion limit exceeded: probably circular dependency"); 81 | 82 | if (!_rulesByTrigger.TryGetValue(propertyName, out var rules)) rules = new List>(); 83 | 84 | var modifiedInThisIteration = new HashSet(); 85 | 86 | foreach (var rule in rules) 87 | { 88 | var targetName = rule.TargetPropertyName; 89 | 90 | if (rule.Updater((TRoot) root)) 91 | { 92 | Trace(rule, propertyName, (TRoot) root); 93 | modified.Add(targetName); 94 | modifiedInThisIteration.Add(targetName); 95 | } 96 | } 97 | 98 | foreach (var name in modifiedInThisIteration) Cascade(name, root, modified, recursionLimit + 1); 99 | } 100 | 101 | 102 | /// 103 | /// Explicitly trigger all the rules. This may be useful if the object is not filled interactively 104 | /// 105 | /// The object on which the rules need to be triggered 106 | public ICollection TriggerAll(object root) 107 | { 108 | var modifiedInThisIteration = new HashSet(); 109 | foreach (var rule in Rules) 110 | { 111 | var targetName = rule.TargetPropertyName; 112 | 113 | if (rule.Updater((TRoot)root)) 114 | { 115 | Trace(rule, "", (TRoot)root); 116 | 117 | modifiedInThisIteration.Add(targetName); 118 | } 119 | } 120 | 121 | var allUpdates = new HashSet(); 122 | 123 | foreach (var name in modifiedInThisIteration) Cascade(name, root, allUpdates, 1); 124 | 125 | return allUpdates; 126 | } 127 | 128 | public int RulesCount => Rules.Count; 129 | 130 | protected virtual void Trace(Rule triggeredRule, string triggerProperty, TRoot instance) 131 | { 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /EngineTests/ExplainTestFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Text; 6 | using EngineTests.TestModelWithInterfaces; 7 | using NUnit.Framework; 8 | using RulesEngine.RulesEngine.Explain; 9 | 10 | namespace EngineTests 11 | { 12 | 13 | [TestFixture] 14 | public class ExplainTestFixture 15 | { 16 | 17 | 18 | class Dog 19 | { 20 | public string Name { get; set; } 21 | public int Age { get; set; } 22 | 23 | 24 | } 25 | 26 | /// 27 | /// This function is never called 28 | /// 29 | /// 30 | /// 31 | bool SomeComplicatedCheck(Abcd abcd) 32 | { 33 | throw new NotImplementedException(); 34 | } 35 | 36 | 37 | int DummyComputer(Abcd parent) 38 | { 39 | return 1; 40 | } 41 | 42 | [Test] 43 | public void ExplainSet() 44 | { 45 | { 46 | Expression> expr = x => DummyComputer(x); 47 | 48 | Console.WriteLine(expr.TryExplain()); 49 | } 50 | 51 | { 52 | Expression> expr = x => 44; 53 | 54 | Console.WriteLine(expr.TryExplain()); 55 | } 56 | 57 | { 58 | Expression> expr = x => x.A; 59 | 60 | Console.WriteLine(expr.TryExplain()); 61 | } 62 | 63 | { 64 | Expression> expr = x => x.A + 1; 65 | 66 | Console.WriteLine(expr.TryExplain()); 67 | } 68 | } 69 | 70 | 71 | [Test] 72 | public void ExplainIf() 73 | { 74 | 75 | { 76 | Expression> expr = x => x.A > 10; 77 | 78 | Console.WriteLine( expr.TryExplain()); 79 | } 80 | 81 | { 82 | Expression> expr = x => x.A > 10 && x.B == 3; 83 | 84 | Console.WriteLine(expr.TryExplain()); 85 | } 86 | 87 | { 88 | Expression> expr = x => x.A > 10 && x.B == 3 || x.C >= 0; 89 | 90 | Console.WriteLine(expr.TryExplain()); 91 | } 92 | 93 | { 94 | var list = new[] {12, 13, 14}; 95 | Expression> expr = x => list.Contains(x.A); 96 | 97 | Console.WriteLine(expr.TryExplain()); 98 | } 99 | 100 | { 101 | var list = new[] { 12, 13, 14 }; 102 | Expression> expr = x => list.Contains(x.A) || x.B == 1; 103 | 104 | Console.WriteLine(expr.TryExplain()); 105 | } 106 | 107 | { 108 | var list = new[] { 12, 13, 14 }; 109 | Expression> expr = x => list.Contains(x.A) && x.B == 1; 110 | 111 | Console.WriteLine(expr.TryExplain()); 112 | } 113 | 114 | { 115 | var listNames = new[] { "Fluffy", "Puffy" }; 116 | var listAges = new[] { 1, 2, 3 }; 117 | 118 | Expression> expr = x => listNames.Contains(x.Name) && listAges.Contains(x.Age); 119 | 120 | Console.WriteLine(expr.TryExplain()); 121 | } 122 | 123 | { 124 | var listNames = new[] { "Fluffy", "Puffy" }; 125 | var listAges = new[] { 1, 2, 3 }; 126 | 127 | Expression> expr = x => listNames.Contains(x.Name) || listAges.Contains(x.Age); 128 | 129 | Console.WriteLine(expr.TryExplain()); 130 | } 131 | 132 | { 133 | 134 | Expression> expr = x => x.Name.StartsWith("Flu") || x.Name.StartsWith("Plu"); 135 | 136 | Console.WriteLine(expr.TryExplain()); 137 | } 138 | 139 | { 140 | 141 | Expression> expr = x => x.Name.StartsWith("Flu") && x.Name.EndsWith("ffy"); 142 | 143 | Console.WriteLine(expr.TryExplain()); 144 | } 145 | 146 | 147 | { 148 | Expression> expr = x => SomeComplicatedCheck(x); 149 | 150 | Console.WriteLine(expr.TryExplain()); 151 | } 152 | } 153 | 154 | 155 | 156 | [Test] 157 | public void ExplainRules() 158 | { 159 | { 160 | var rules = new AbcdRules(); 161 | 162 | foreach (var rule in rules.Rules) 163 | { 164 | Console.WriteLine(rule); 165 | } 166 | } 167 | 168 | Console.WriteLine(); 169 | 170 | { 171 | var rules = new DogRules(); 172 | 173 | foreach (var rule in rules.Rules) 174 | { 175 | Console.WriteLine(rule); 176 | } 177 | } 178 | 179 | Console.WriteLine(); 180 | 181 | { 182 | var rules = new CdsRules(); 183 | 184 | foreach (var rule in rules.Rules) 185 | { 186 | Console.WriteLine(rule); 187 | } 188 | } 189 | } 190 | 191 | 192 | 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /RulesEngine/Tools/CompiledAccessors.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq.Expressions; 5 | using System.Reflection; 6 | 7 | namespace RulesEngine.Tools 8 | { 9 | /// 10 | /// Caches a compiled version for the public accessors of a type 11 | /// We have getters, setters and smart setters; the last one changes the value only if it os different and returns true 12 | /// only if 13 | /// the value was changed 14 | /// 15 | public static class CompiledAccessors 16 | { 17 | private static readonly Dictionary> CompiledGetters = 18 | new Dictionary>(); 19 | 20 | private static readonly Dictionary> CompiledSetters = 21 | new Dictionary>(); 22 | 23 | private static readonly Dictionary> CompiledSmartSetters = 24 | new Dictionary>(); 25 | 26 | private static string MakeKey(Type declaringType, string propertyName) 27 | { 28 | return declaringType.FullName + "." + propertyName; 29 | } 30 | 31 | public static Func CompiledGetter(Type declaringType, string propertyName) 32 | { 33 | var key = MakeKey(declaringType, propertyName); 34 | 35 | 36 | lock (CompiledGetters) 37 | { 38 | if (CompiledGetters.ContainsKey(key)) 39 | { 40 | return CompiledGetters[key]; 41 | } 42 | 43 | return CompiledGetters[key] = CompileGetter(declaringType.GetProperty(propertyName)); 44 | } 45 | } 46 | 47 | public static Action CompiledSetter(Type declaringType, string propertyName) 48 | { 49 | var key = MakeKey(declaringType, propertyName); 50 | 51 | lock (CompiledSetters) 52 | { 53 | if (CompiledSetters.ContainsKey(key)) 54 | { 55 | return CompiledSetters[key]; 56 | } 57 | 58 | return CompiledSetters[key] = CompileSetter(declaringType.GetProperty(propertyName)); 59 | } 60 | } 61 | 62 | public static Func CompiledSmartSetter(Type declaringType, string propertyName) 63 | { 64 | var key = MakeKey(declaringType, propertyName); 65 | 66 | lock (CompiledSmartSetters) 67 | { 68 | if (CompiledSmartSetters.ContainsKey(key)) 69 | { 70 | return CompiledSmartSetters[key]; 71 | } 72 | 73 | return CompiledSmartSetters[key] = CompileSmartSetter(declaringType.GetProperty(propertyName)); 74 | } 75 | } 76 | 77 | /// 78 | /// Precompile a call to a property getter. It can be called to avoid reflexion base invocation 79 | /// 80 | /// 81 | private static Func CompileGetter(PropertyInfo propertyInfo) 82 | { 83 | var instance = Expression.Parameter(typeof (object), "instance"); 84 | 85 | Debug.Assert(propertyInfo.DeclaringType != null, "propertyInfo.DeclaringType != null"); 86 | var instanceCast = propertyInfo.DeclaringType.IsValueType 87 | ? Expression.TypeAs(instance, propertyInfo.DeclaringType) 88 | : Expression.Convert(instance, propertyInfo.DeclaringType); 89 | 90 | return 91 | Expression.Lambda>( 92 | Expression.TypeAs(Expression.Call(instanceCast, propertyInfo.GetGetMethod()), typeof (object)), 93 | instance) 94 | .Compile(); 95 | } 96 | 97 | /// 98 | /// Precompile a call to a property setter. It can be called to avoid reflexion based invocation 99 | /// 100 | /// 101 | /// 102 | private static Action CompileSetter(PropertyInfo propertyInfo) 103 | { 104 | var instance = Expression.Parameter(typeof (object), "instance"); 105 | 106 | var value = Expression.Parameter(typeof (object), "value"); 107 | 108 | var instanceType = propertyInfo.DeclaringType; 109 | var propertyType = propertyInfo.PropertyType; 110 | 111 | Debug.Assert(instanceType != null, "instanceType != null"); 112 | var instanceCast = Expression.Convert(instance, instanceType); 113 | 114 | var valueCast = Expression.Convert(value, propertyType); 115 | 116 | return 117 | Expression.Lambda>( 118 | Expression.Call(instanceCast, propertyInfo.GetSetMethod(), valueCast), instance, value) 119 | .Compile(); 120 | } 121 | 122 | /// 123 | /// Smart setter that changes a value only if it is different from the current one 124 | /// 125 | /// true if value changed, false if same 126 | private static Func CompileSmartSetter(PropertyInfo propertyInfo) 127 | { 128 | var getter = CompiledGetter(propertyInfo.DeclaringType, propertyInfo.Name); 129 | var setter = CompiledSetter(propertyInfo.DeclaringType, propertyInfo.Name); 130 | 131 | return (parent, val) => 132 | { 133 | var previous = getter(parent); 134 | if (!Equals(previous, val)) 135 | { 136 | setter(parent, val); 137 | return true; 138 | } 139 | 140 | return false; 141 | }; 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /RulesEngine/RulesEngine/FluentExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq.Expressions; 5 | using RulesEngine.RulesEngine.Explain; 6 | using RulesEngine.Tools; 7 | 8 | namespace RulesEngine.RulesEngine 9 | { 10 | /// 11 | /// Implement a fluent syntax o define business rules 12 | /// Each rule has: 13 | /// - exactly one target property (that CAN be modified by the rule) 14 | /// - one or more trigger properties (when they are changed the rule is triggered) 15 | /// - an optional condition; the execution is blocked if it is false 16 | /// - a value calculator: pure function that computes the value for the target property 17 | /// 18 | public static class FluentExtensions 19 | { 20 | /// 21 | /// Second part of the rule declaration; specifies the function that will compute the value of the target property 22 | /// 23 | /// 24 | /// 25 | /// 26 | /// 27 | /// optionally provide a human readable description 28 | /// 29 | public static FluentToken With( 30 | this FluentToken token, Expression> valueComputer, string manualExplain = null) 31 | { 32 | token.ValueComputerExplained = manualExplain ?? valueComputer.TryExplain(); 33 | token.ValueComputer = valueComputer.Compile(); 34 | 35 | return token; 36 | } 37 | 38 | /// 39 | /// Specifies the first trigger property 40 | /// 41 | /// 42 | /// 43 | /// 44 | /// 45 | /// 46 | public static void OnChanged( 47 | this FluentToken token, params Expression>[] propertySelectors) 48 | { 49 | 50 | foreach (var propertySelector in propertySelectors) 51 | { 52 | token.PropertyNames.Add(ExpressionTreeHelper.PropertyName(propertySelector)); 53 | } 54 | 55 | var propertyName = ExpressionTreeHelper.PropertyName(token.TargetPropertySelector); 56 | 57 | bool Updater(TParent parent) 58 | { 59 | if (token.IfPredicate != null) 60 | if (!token.IfPredicate(parent)) 61 | return false; 62 | 63 | var newValue = token.ValueComputer(parent); 64 | 65 | var oldValue = RuntimeCompiler.Getter(token.TargetPropertySelector)(parent); 66 | if (Equals(oldValue, newValue)) return false; 67 | 68 | RuntimeCompiler.SetterFromGetter(token.TargetPropertySelector)(parent, 69 | newValue); 70 | 71 | return true; 72 | } 73 | 74 | var rule = new Rule 75 | { 76 | TriggerProperties = token.PropertyNames, 77 | Updater = Updater, 78 | TargetPropertyName = propertyName, 79 | IfExplained = token.IfExplained, 80 | ValueComputerExplained = token.ValueComputerExplained 81 | 82 | }; 83 | 84 | 85 | token.MappingRulesContainer.Rules.Add(rule); 86 | 87 | foreach (var name in token.PropertyNames) 88 | { 89 | if (!token.MappingRulesContainer.RulesByTrigger.TryGetValue(name, out var rules)) 90 | { 91 | rules = new List>(); 92 | token.MappingRulesContainer.RulesByTrigger.Add(name, rules); 93 | } 94 | 95 | rules.Add(rule); 96 | } 97 | } 98 | 99 | 100 | /// 101 | /// Specifies an optional applicability condition as a predicate; if false the rule will not be triggered 102 | /// 103 | /// 104 | /// 105 | /// 106 | /// 107 | /// optionally provide a human readable description 108 | /// 109 | public static FluentToken If( 110 | this FluentToken token, Expression> ifPredicate, string manualExplain = null) 111 | { 112 | token.IfPredicate = ifPredicate.Compile(); 113 | token.IfExplained = manualExplain ?? ifPredicate.TryExplain(); 114 | 115 | return token; 116 | } 117 | 118 | 119 | 120 | /// 121 | /// Internally used to chain the statements of the fluent syntax 122 | /// 123 | /// 124 | /// 125 | public class FluentToken 126 | { 127 | private HashSet _propertyNames; 128 | 129 | public MappingRules MappingRulesContainer { get; set; } 130 | 131 | public Func ValueComputer { get; set; } 132 | 133 | public Func IfPredicate { get; set; } 134 | 135 | public ISet PropertyNames => _propertyNames ?? (_propertyNames = new HashSet()); 136 | 137 | public Expression> TargetPropertySelector { get; set; } 138 | public string ValueComputerExplained { get; set; } 139 | public string IfExplained { get; set; } 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /RulesEngine/RulesEngine/Explain/QueryVisitor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using System.Security.Cryptography; 7 | using System.Security.Cryptography.X509Certificates; 8 | using System.Text; 9 | using Remotion.Linq; 10 | using Remotion.Linq.Clauses; 11 | using Remotion.Linq.Clauses.Expressions; 12 | using Remotion.Linq.Clauses.ResultOperators; 13 | 14 | namespace RulesEngine.RulesEngine.Explain 15 | { 16 | public class QueryVisitor : QueryModelVisitorBase 17 | { 18 | 19 | public QueryVisitor() 20 | { 21 | RootExpression = new ExplainOrExpression(); 22 | } 23 | 24 | public ExplainOrExpression RootExpression { get; set; } 25 | 26 | 27 | private bool IsLeafExpression(Expression expression) 28 | { 29 | return expression.NodeType == ExpressionType.GreaterThan 30 | || expression.NodeType == ExpressionType.GreaterThanOrEqual 31 | || expression.NodeType == ExpressionType.LessThan 32 | || expression.NodeType == ExpressionType.LessThanOrEqual 33 | || expression.NodeType == ExpressionType.Equal 34 | || expression.NodeType == ExpressionType.NotEqual; 35 | } 36 | 37 | 38 | public override void VisitSelectClause(SelectClause selectClause, QueryModel queryModel) 39 | { 40 | base.VisitSelectClause(selectClause, queryModel); 41 | } 42 | 43 | 44 | 45 | public override void VisitWhereClause(WhereClause whereClause, QueryModel queryModel, int index) 46 | { 47 | if (whereClause.Predicate is BinaryExpression expression) 48 | { 49 | VisitBinaryExpression(expression, RootExpression); 50 | } 51 | else 52 | { 53 | if (whereClause.Predicate is SubQueryExpression subQuery) 54 | { 55 | ExplainAndExpression andExpression; 56 | 57 | if (!RootExpression.MultipleWhereClauses) 58 | { 59 | andExpression = new ExplainAndExpression(); 60 | RootExpression.Elements.Add(andExpression); 61 | } 62 | else // multiple where clauses are joined by AND 63 | { 64 | andExpression = RootExpression.Elements[0]; 65 | } 66 | 67 | var leaf = new LeafExpression(); 68 | andExpression.Elements.Add(leaf); 69 | 70 | VisitContainsExpression(subQuery, leaf); 71 | } 72 | else if (whereClause.Predicate is MethodCallExpression call) 73 | { 74 | ExplainAndExpression andExpression; 75 | 76 | if (!RootExpression.MultipleWhereClauses) 77 | { 78 | andExpression = new ExplainAndExpression(); 79 | RootExpression.Elements.Add(andExpression); 80 | } 81 | else // multiple where clauses are joined by AND 82 | { 83 | andExpression = RootExpression.Elements[0]; 84 | } 85 | 86 | var leaf = CallToLeafExpression(call); 87 | andExpression.Elements.Add(leaf); 88 | 89 | } 90 | else if(whereClause.Predicate.NodeType != ExpressionType.Constant) 91 | { 92 | throw new NotSupportedException("query too complex"); 93 | } 94 | } 95 | 96 | 97 | RootExpression.MultipleWhereClauses = true; 98 | 99 | base.VisitWhereClause(whereClause, queryModel, index); 100 | } 101 | 102 | 103 | 104 | private void VisitAndExpression(BinaryExpression binaryExpression, ExplainAndExpression andExpression) 105 | { 106 | if (IsLeafExpression(binaryExpression.Left)) 107 | { 108 | andExpression.Elements.Add(VisitLeafExpression((BinaryExpression)binaryExpression.Left)); 109 | } 110 | else if (binaryExpression.Left.NodeType == ExpressionType.AndAlso) 111 | { 112 | VisitAndExpression((BinaryExpression)binaryExpression.Left, andExpression); 113 | } 114 | else if (binaryExpression.Left.NodeType == ExpressionType.Extension) 115 | { 116 | if (binaryExpression.Left is SubQueryExpression subQuery) 117 | { 118 | var leaf = new LeafExpression(); 119 | andExpression.Elements.Add(leaf); 120 | VisitContainsExpression(subQuery, leaf); 121 | } 122 | } 123 | else if (binaryExpression.Left is MethodCallExpression callExpression) 124 | { 125 | andExpression.Elements.Add(CallToLeafExpression(callExpression)); 126 | } 127 | else 128 | { 129 | throw new NotSupportedException("ExplainExpression too complex"); 130 | } 131 | 132 | if (IsLeafExpression(binaryExpression.Right)) 133 | { 134 | andExpression.Elements.Add(VisitLeafExpression((BinaryExpression)binaryExpression.Right)); 135 | } 136 | else if (binaryExpression.Right.NodeType == ExpressionType.Extension) 137 | { 138 | if (binaryExpression.Right is SubQueryExpression subQuery) 139 | { 140 | var leaf = new LeafExpression(); 141 | andExpression.Elements.Add(leaf); 142 | VisitContainsExpression(subQuery, leaf); 143 | } 144 | } 145 | else if (binaryExpression.Right is MethodCallExpression callExpression) 146 | { 147 | andExpression.Elements.Add(CallToLeafExpression(callExpression)); 148 | } 149 | else 150 | { 151 | throw new NotSupportedException("ExplainExpression too complex"); 152 | } 153 | } 154 | 155 | private void VisitContainsExpression(SubQueryExpression subQuery, LeafExpression leaf) 156 | { 157 | if (subQuery.QueryModel.ResultOperators.Count != 1) 158 | throw new NotSupportedException("Only Contains extension is supported"); 159 | 160 | var contains = subQuery.QueryModel.ResultOperators[0] as ContainsResultOperator; 161 | 162 | // process collection.Contains(x=>x.Member) 163 | if (contains?.Item is MemberExpression item) 164 | { 165 | var select = subQuery.QueryModel?.SelectClause.Selector as QuerySourceReferenceExpression; 166 | 167 | leaf.MemberName = item.Member.Name; 168 | 169 | if (select?.ReferencedQuerySource is MainFromClause from) 170 | { 171 | var expression = from.FromExpression as ConstantExpression; 172 | 173 | if (expression?.Value is IEnumerable values) 174 | { 175 | leaf.Operator = QueryOperator.In; 176 | 177 | foreach (var value in values) 178 | { 179 | leaf.InValues.Add(value); 180 | } 181 | 182 | return; 183 | } 184 | } 185 | } 186 | // process x=>x.VectorMember.Contains(value) 187 | else 188 | { 189 | var value = contains?.Item; 190 | 191 | if (value != null) 192 | { 193 | var select = subQuery.QueryModel?.SelectClause.Selector as QuerySourceReferenceExpression; 194 | var from = select?.ReferencedQuerySource as MainFromClause; 195 | 196 | 197 | if (from?.FromExpression is MemberExpression expression) 198 | { 199 | // the member must not be a scalar type. A string is a vector of chars but still considered a scalar in this context 200 | var isVector = typeof(IEnumerable).IsAssignableFrom(expression.Type) && 201 | !typeof(string).IsAssignableFrom(expression.Type); 202 | 203 | if (!isVector) 204 | throw new NotSupportedException("Trying to use Contains extension on a scalar member"); 205 | 206 | 207 | if (value is ConstantExpression valueExpession) 208 | { 209 | leaf.Operator = QueryOperator.In; 210 | 211 | leaf.InValues.Add(valueExpession.Value); 212 | 213 | return; 214 | } 215 | } 216 | } 217 | } 218 | 219 | throw new NotSupportedException("Only Contains extension is supported"); 220 | } 221 | 222 | private void VisitBinaryExpression(BinaryExpression binaryExpression, ExplainOrExpression rootExpression) 223 | { 224 | // manage AND expressions 225 | if (binaryExpression.NodeType == ExpressionType.AndAlso) 226 | { 227 | var andExpression = new ExplainAndExpression(); 228 | rootExpression.Elements.Add(andExpression); 229 | 230 | VisitAndExpression(binaryExpression, andExpression); 231 | } 232 | 233 | // manage OR expressions 234 | else if (binaryExpression.NodeType == ExpressionType.OrElse) 235 | { 236 | VisitOrExpression(binaryExpression, rootExpression); 237 | } 238 | 239 | // manage simple expressions like a > 10 240 | else if (IsLeafExpression(binaryExpression)) 241 | { 242 | ExplainAndExpression andExpression; 243 | 244 | if (!rootExpression.MultipleWhereClauses) 245 | { 246 | andExpression = new ExplainAndExpression(); 247 | rootExpression.Elements.Add(andExpression); 248 | } 249 | else // if multiple where clauses consider them as expressions linked by AND 250 | { 251 | andExpression = rootExpression.Elements[0]; 252 | } 253 | 254 | 255 | andExpression.Elements.Add(VisitLeafExpression(binaryExpression)); 256 | } 257 | else 258 | { 259 | throw new NotSupportedException("ExplainExpression too complex"); 260 | } 261 | } 262 | 263 | //TODO add unit test for OR expression with Contains 264 | /// 265 | /// OR expression can be present only at root level 266 | /// 267 | /// 268 | /// 269 | private void VisitOrExpression(BinaryExpression binaryExpression, ExplainOrExpression rootExpression) 270 | { 271 | // visit left part 272 | if (IsLeafExpression(binaryExpression.Left)) 273 | { 274 | var andExpression = new ExplainAndExpression(); 275 | rootExpression.Elements.Add(andExpression); 276 | 277 | andExpression.Elements.Add(VisitLeafExpression((BinaryExpression)binaryExpression.Left)); 278 | } 279 | else if (binaryExpression.Left.NodeType == ExpressionType.AndAlso) 280 | { 281 | var andExpression = new ExplainAndExpression(); 282 | rootExpression.Elements.Add(andExpression); 283 | VisitAndExpression((BinaryExpression)binaryExpression.Left, andExpression); 284 | } 285 | else if (binaryExpression.Left.NodeType == ExpressionType.Extension) 286 | { 287 | if (binaryExpression.Left is SubQueryExpression subQuery) 288 | { 289 | ExplainAndExpression andExpression; 290 | 291 | if (!rootExpression.MultipleWhereClauses) 292 | { 293 | andExpression = new ExplainAndExpression(); 294 | rootExpression.Elements.Add(andExpression); 295 | } 296 | else // multiple where clauses are joined by AND 297 | { 298 | andExpression = rootExpression.Elements[0]; 299 | } 300 | 301 | 302 | var leaf = new LeafExpression(); 303 | andExpression.Elements.Add(leaf); 304 | 305 | VisitContainsExpression(subQuery, leaf); 306 | } 307 | } 308 | else if (binaryExpression.Left.NodeType == ExpressionType.OrElse) 309 | { 310 | VisitOrExpression((BinaryExpression)binaryExpression.Left, rootExpression); 311 | } 312 | else if (binaryExpression.Left.NodeType == ExpressionType.AndAlso) 313 | { 314 | var andExpression = new ExplainAndExpression(); 315 | rootExpression.Elements.Add(andExpression); 316 | VisitAndExpression((BinaryExpression)binaryExpression.Left, andExpression); 317 | } 318 | else if (binaryExpression.Left is MethodCallExpression call) 319 | { 320 | ExplainAndExpression andExpression; 321 | 322 | if (!RootExpression.MultipleWhereClauses) 323 | { 324 | andExpression = new ExplainAndExpression(); 325 | RootExpression.Elements.Add(andExpression); 326 | } 327 | else // multiple where clauses are joined by AND 328 | { 329 | andExpression = RootExpression.Elements[0]; 330 | } 331 | 332 | var leaf = CallToLeafExpression(call); 333 | andExpression.Elements.Add(leaf); 334 | 335 | } 336 | else 337 | { 338 | throw new NotSupportedException("ExplainExpression too complex"); 339 | } 340 | 341 | // visit right part 342 | if (IsLeafExpression(binaryExpression.Right)) 343 | { 344 | var andExpression = new ExplainAndExpression(); 345 | rootExpression.Elements.Add(andExpression); 346 | 347 | andExpression.Elements.Add(VisitLeafExpression((BinaryExpression)binaryExpression.Right)); 348 | } 349 | else if (binaryExpression.Right.NodeType == ExpressionType.Extension) 350 | { 351 | if (binaryExpression.Right is SubQueryExpression subQuery) 352 | { 353 | var andExpression = new ExplainAndExpression(); 354 | rootExpression.Elements.Add(andExpression); 355 | 356 | if (rootExpression.MultipleWhereClauses) 357 | throw new NotSupportedException( 358 | "Multiple where clauses can be used only with simple expressions"); 359 | 360 | 361 | var leaf = new LeafExpression(); 362 | andExpression.Elements.Add(leaf); 363 | VisitContainsExpression(subQuery, leaf); 364 | } 365 | } 366 | else if (binaryExpression.Right.NodeType == ExpressionType.OrElse) 367 | { 368 | VisitOrExpression((BinaryExpression)binaryExpression.Right, rootExpression); 369 | } 370 | else if (binaryExpression.Right.NodeType == ExpressionType.AndAlso) 371 | { 372 | var andExpression = new ExplainAndExpression(); 373 | rootExpression.Elements.Add(andExpression); 374 | VisitAndExpression((BinaryExpression)binaryExpression.Right, andExpression); 375 | } 376 | else if (binaryExpression.Right is MethodCallExpression call) 377 | { 378 | ExplainAndExpression andExpression; 379 | 380 | if (!RootExpression.MultipleWhereClauses) 381 | { 382 | andExpression = new ExplainAndExpression(); 383 | RootExpression.Elements.Add(andExpression); 384 | } 385 | else // multiple where clauses are joined by AND 386 | { 387 | andExpression = RootExpression.Elements[0]; 388 | } 389 | 390 | var leaf = CallToLeafExpression(call); 391 | andExpression.Elements.Add(leaf); 392 | 393 | } 394 | else 395 | { 396 | throw new NotSupportedException("ExplainExpression too complex"); 397 | } 398 | } 399 | 400 | private static LeafExpression CallToLeafExpression(MethodCallExpression call) 401 | { 402 | StringBuilder sb = new StringBuilder(); 403 | sb.Append(call.Method.Name); 404 | sb.Append("("); 405 | 406 | foreach (var argument in call.Arguments) 407 | { 408 | if (argument is ConstantExpression constant) 409 | { 410 | sb.Append(constant.Value); 411 | } 412 | } 413 | 414 | sb.Append(")"); 415 | 416 | 417 | var leaf = new LeafExpression {MethodCall = sb.ToString()}; 418 | return leaf; 419 | } 420 | 421 | // TODO add unit test for reverted expression : const = member 422 | 423 | /// 424 | /// Manage simple expressions like left operator right 425 | /// 426 | /// 427 | private LeafExpression VisitLeafExpression(BinaryExpression binaryExpression) 428 | { 429 | if (binaryExpression.Left is MemberExpression left && binaryExpression.Right is ConstantExpression right) 430 | { 431 | 432 | var oper = QueryOperator.Eq; 433 | 434 | 435 | if (binaryExpression.NodeType == ExpressionType.GreaterThan) oper = QueryOperator.Gt; 436 | 437 | if (binaryExpression.NodeType == ExpressionType.GreaterThanOrEqual) oper = QueryOperator.Ge; 438 | 439 | if (binaryExpression.NodeType == ExpressionType.LessThan) oper = QueryOperator.Lt; 440 | 441 | if (binaryExpression.NodeType == ExpressionType.LessThanOrEqual) oper = QueryOperator.Le; 442 | 443 | if (binaryExpression.NodeType == ExpressionType.NotEqual) oper = QueryOperator.NotEqual; 444 | 445 | return new LeafExpression(left.Member.Name, right.Value, oper); 446 | } 447 | 448 | // try to revert the expression 449 | left = binaryExpression.Right as MemberExpression; 450 | right = binaryExpression.Left as ConstantExpression; 451 | 452 | if (left != null && right != null) 453 | { 454 | 455 | 456 | var oper = QueryOperator.Eq; 457 | 458 | 459 | if (binaryExpression.NodeType == ExpressionType.GreaterThan) oper = QueryOperator.Le; 460 | 461 | if (binaryExpression.NodeType == ExpressionType.GreaterThanOrEqual) oper = QueryOperator.Lt; 462 | 463 | if (binaryExpression.NodeType == ExpressionType.LessThan) oper = QueryOperator.Ge; 464 | 465 | if (binaryExpression.NodeType == ExpressionType.LessThanOrEqual) oper = QueryOperator.Gt; 466 | 467 | return new LeafExpression(left.Member.Name, right.Value, oper); 468 | } 469 | 470 | throw new NotSupportedException("Error parsing binary expression"); 471 | } 472 | } 473 | } --------------------------------------------------------------------------------