├── .gitignore ├── MicroRuleEngine.Core.Tests ├── ExampleUsage.cs ├── ExpressionToSQLQueryTest.cs ├── InMemoryEntityFrameworkTests.cs ├── MicroRuleEngine.Core.Tests.csproj └── Models │ ├── Blog.cs │ └── Order.cs ├── MicroRuleEngine.Tests ├── DataRowTest.cs ├── ExampleUsage.cs ├── ExceptionTests.cs ├── IsTypeTests.cs ├── MicroRuleEngine.Tests.csproj ├── Models │ ├── MemberOperaterMemberTestObject.cs │ └── Order.cs ├── NewAPI.cs ├── Properties │ └── AssemblyInfo.cs ├── SerializationTests.cs └── TimeTest.cs ├── MicroRuleEngine.sln ├── MicroRuleEngine ├── MRE.cs └── MicroRuleEngine.csproj ├── Nuget ├── CreatePackage.bat └── MRE.nuspec ├── README.md └── license.txt /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | TestResults 4 | TestResult.xml 5 | content 6 | *.nupkg 7 | *.suo 8 | *.log 9 | *.cache 10 | *.user 11 | *.tmp 12 | *.ldf 13 | *.mdf 14 | *.swx 15 | *.vsmdi 16 | *.testsettings 17 | /.vs 18 | /ConsoleApp1 19 | /packages 20 | /MicroRuleEngine.Tests.psess 21 | /ConsoleApp1.psess 22 | /ConsoleApp1-3.psess 23 | /ConsoleApp1-2.psess 24 | /ConsoleApp1-1.psess 25 | /MicroRuleEngine/MRE1.cs 26 | privateKey.key 27 | -------------------------------------------------------------------------------- /MicroRuleEngine.Core.Tests/ExampleUsage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using MicroRuleEngine.Core.Tests.Models; 7 | 8 | namespace MicroRuleEngine.Tests 9 | { 10 | /// 11 | /// Summary description for UnitTest1 12 | /// 13 | [TestClass] 14 | public class ExampleUsage 15 | { 16 | [TestMethod] 17 | public void ChildPropertiesOfNull() 18 | { 19 | Order order = GetOrder(); 20 | order.Customer = null; 21 | Rule rule = new Rule 22 | { 23 | MemberName = "Customer.Country.CountryCode", 24 | Operator = ExpressionType.Equal.ToString("g"), 25 | TargetValue = "AUS" 26 | }; 27 | MRE engine = new MRE(); 28 | var compiledRule = engine.CompileRule(rule); 29 | bool passes = compiledRule(order); 30 | Assert.IsFalse(passes); 31 | } 32 | 33 | [TestMethod] 34 | public void GetAllField() 35 | { 36 | Order order = GetOrder(); 37 | 38 | var type = order.GetType(); 39 | var members = MRE.Member.GetFields(type); 40 | Assert.IsTrue( 41 | members.Where(x=> x.Name == "Customer.Country.CountryCode" && x.PossibleOperators.Any(y=> y.Name == "StartsWith")).Any() 42 | ); 43 | } 44 | 45 | [TestMethod] 46 | public void CoerceMethod() 47 | { 48 | Order order = GetOrder(); 49 | Rule rule = new Rule 50 | { 51 | MemberName = "Codes", 52 | Operator = "Contains", 53 | TargetValue = "243", 54 | Inputs = new List() { "243" } 55 | }; 56 | MRE engine = new MRE(); 57 | var compiledRule = engine.CompileRule(rule); 58 | bool passes = compiledRule(order); 59 | Assert.IsTrue(passes); 60 | 61 | order.Codes.Clear(); 62 | passes = compiledRule(order); 63 | Assert.IsFalse(passes); 64 | } 65 | 66 | [TestMethod] 67 | public void ChildProperties() 68 | { 69 | Order order = GetOrder(); 70 | Rule rule = new Rule 71 | { 72 | MemberName = "Customer.Country.CountryCode", 73 | Operator = ExpressionType.Equal.ToString("g"), 74 | TargetValue = "AUS" 75 | }; 76 | MRE engine = new MRE(); 77 | var compiledRule = engine.CompileRule(rule); 78 | bool passes = compiledRule(order); 79 | Assert.IsTrue(passes); 80 | 81 | order.Customer.Country.CountryCode = "USA"; 82 | passes = compiledRule(order); 83 | Assert.IsFalse(passes); 84 | } 85 | 86 | [TestMethod] 87 | 88 | public void ConditionalLogic() 89 | { 90 | Order order = GetOrder(); 91 | Rule rule = new Rule 92 | { 93 | Operator = ExpressionType.AndAlso.ToString("g"), 94 | Rules = new List 95 | { 96 | new Rule { MemberName = "Customer.LastName", TargetValue = "Doe", Operator = "Equal"}, 97 | new Rule 98 | { 99 | Operator = "Or", 100 | Rules = new List 101 | { 102 | new Rule { MemberName = "Customer.FirstName", TargetValue = "John", Operator = "Equal"}, 103 | new Rule { MemberName = "Customer.FirstName", TargetValue = "Jane", Operator = "Equal"} 104 | } 105 | } 106 | } 107 | }; 108 | MRE engine = new MRE(); 109 | var fakeName = engine.CompileRule(rule); 110 | bool passes = fakeName(order); 111 | Assert.IsTrue(passes); 112 | 113 | order.Customer.FirstName = "Philip"; 114 | passes = fakeName(order); 115 | Assert.IsFalse(passes); 116 | } 117 | 118 | [TestMethod] 119 | public void BooleanMethods() 120 | { 121 | Order order = GetOrder(); 122 | Rule rule = new Rule 123 | { 124 | Operator = "HasItem",//The Order Object Contains a method named 'HasItem' that returns true/false 125 | Inputs = new List { "Test" } 126 | }; 127 | MRE engine = new MRE(); 128 | 129 | var boolMethod = engine.CompileRule(rule); 130 | bool passes = boolMethod(order); 131 | Assert.IsTrue(passes); 132 | 133 | var item = order.Items.First(x => x.ItemCode == "Test"); 134 | item.ItemCode = "Changed"; 135 | passes = boolMethod(order); 136 | Assert.IsFalse(passes); 137 | } 138 | 139 | [TestMethod] 140 | public void BooleanMethods_ByType() 141 | { 142 | Order order = GetOrder(); 143 | Rule rule = new Rule 144 | { 145 | Operator = "HasItem",//The Order Object Contains a method named 'HasItem' that returns true/false 146 | Inputs = new List { "Test" } 147 | }; 148 | MRE engine = new MRE(); 149 | 150 | var boolMethod = engine.CompileRule(typeof(Order), rule); 151 | bool passes =(bool) boolMethod.DynamicInvoke(order); 152 | Assert.IsTrue(passes); 153 | 154 | var item = order.Items.First(x => x.ItemCode == "Test"); 155 | item.ItemCode = "Changed"; 156 | passes = (bool)boolMethod.DynamicInvoke(order); 157 | Assert.IsFalse(passes); 158 | } 159 | 160 | [TestMethod] 161 | public void AnyOperator() 162 | { 163 | Order order = GetOrder(); 164 | //order.Items.Any(a => a.ItemCode == "test") 165 | Rule rule = new Rule 166 | { 167 | MemberName = "Items",// The array property 168 | Operator = "Any", 169 | Rules = new[] 170 | { 171 | new Rule 172 | { 173 | MemberName = "ItemCode", // the property in the above array item 174 | Operator = "Equal", 175 | TargetValue = "Test", 176 | } 177 | } 178 | }; 179 | MRE engine = new MRE(); 180 | var boolMethod = engine.CompileRule(rule); 181 | bool passes = boolMethod(order); 182 | Assert.IsTrue(passes); 183 | 184 | var item = order.Items.First(x => x.ItemCode == "Test"); 185 | item.ItemCode = "Changed"; 186 | passes = boolMethod(order); 187 | Assert.IsFalse(passes); 188 | } 189 | 190 | 191 | 192 | [TestMethod] 193 | public void ChildPropertyBooleanMethods() 194 | { 195 | Order order = GetOrder(); 196 | Rule rule = new Rule 197 | { 198 | MemberName = "Customer.FirstName", 199 | Operator = "EndsWith",//Regular method that exists on string.. As a note expression methods are not available 200 | Inputs = new List { "ohn" } 201 | }; 202 | MRE engine = new MRE(); 203 | var childPropCheck = engine.CompileRule(rule); 204 | bool passes = childPropCheck(order); 205 | Assert.IsTrue(passes); 206 | 207 | order.Customer.FirstName = "jane"; 208 | passes = childPropCheck(order); 209 | Assert.IsFalse(passes); 210 | } 211 | 212 | [TestMethod] 213 | public void ChildPropertyOfNullBooleanMethods() 214 | { 215 | Order order = GetOrder(); 216 | order.Customer = null; 217 | Rule rule = new Rule 218 | { 219 | MemberName = "Customer.FirstName", 220 | Operator = "EndsWith", //Regular method that exists on string.. As a note expression methods are not available 221 | Inputs = new List { "ohn" } 222 | }; 223 | MRE engine = new MRE(); 224 | var childPropCheck = engine.CompileRule(rule); 225 | bool passes = childPropCheck(order); 226 | Assert.IsFalse(passes); 227 | } 228 | [TestMethod] 229 | public void RegexIsMatch()//Had to add a Regex evaluator to make it feel 'Complete' 230 | { 231 | Order order = GetOrder(); 232 | Rule rule = new Rule 233 | { 234 | MemberName = "Customer.FirstName", 235 | Operator = "IsMatch", 236 | TargetValue = @"^[a-zA-Z0-9]*$" 237 | }; 238 | MRE engine = new MRE(); 239 | var regexCheck = engine.CompileRule(rule); 240 | bool passes = regexCheck(order); 241 | Assert.IsTrue(passes); 242 | 243 | order.Customer.FirstName = "--NoName"; 244 | passes = regexCheck(order); 245 | Assert.IsFalse(passes); 246 | } 247 | 248 | public static Order GetOrder() 249 | { 250 | Order order = new Order() 251 | { 252 | OrderId = 1, 253 | Customer = new Customer() 254 | { 255 | FirstName = "John", 256 | LastName = "Doe", 257 | Country = new Country() 258 | { 259 | CountryCode = "AUS" 260 | } 261 | }, 262 | Items = new List() 263 | { 264 | new Item { ItemCode = "MM23", Cost=5.25M}, 265 | new Item { ItemCode = "LD45", Cost=5.25M}, 266 | new Item { ItemCode = "Test", Cost=3.33M}, 267 | }, 268 | Codes = new List() 269 | { 270 | 555, 271 | 321, 272 | 243 273 | }, 274 | Total = 13.83m, 275 | OrderDate = new DateTime(1776, 7, 4), 276 | Status = Status.Open 277 | 278 | }; 279 | return order; 280 | } 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /MicroRuleEngine.Core.Tests/ExpressionToSQLQueryTest.cs: -------------------------------------------------------------------------------- 1 | using EFModeling.Samples.DataSeeding; 2 | using Microsoft.Data.Sqlite; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace MicroRuleEngine.Core.Tests 12 | { 13 | [TestClass] 14 | public class ExpressionToSQLQueryTest 15 | { 16 | internal DbContextOptions GetDBOptions() 17 | { 18 | var connection = new SqliteConnection("DataSource=:memory:"); 19 | connection.Open(); 20 | var options = new DbContextOptionsBuilder() 21 | .UseSqlite(connection) 22 | .Options; 23 | 24 | // Create the schema in the database 25 | using (var context = new BloggingContext(options)) 26 | { 27 | context.Database.EnsureCreated(); 28 | } 29 | return options; 30 | } 31 | [TestMethod] 32 | public void BasicEqualityExpression() 33 | { 34 | 35 | using (var context = new BloggingContext(GetDBOptions())) 36 | { 37 | context.Blogs.Add(new Blog { Url = "http://test.com" }); 38 | context.SaveChanges(); 39 | 40 | var testBlog = context.Blogs.FirstOrDefault(b => b.Url == "http://test.com"); 41 | 42 | var fields = MRE.Member.GetFields(typeof(Blog)); 43 | Rule rule = new Rule 44 | { 45 | MemberName = "Url", 46 | Operator = mreOperator.Equal.ToString("g"), 47 | TargetValue = "http://test.com" 48 | }; 49 | 50 | var blog2 = context.Blogs.Where(MRE.ToExpression(rule, false)).FirstOrDefault(); 51 | 52 | Assert.IsTrue(testBlog.BlogId == blog2.BlogId); 53 | 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /MicroRuleEngine.Core.Tests/InMemoryEntityFrameworkTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jeremy Oursler All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.ComponentModel.DataAnnotations; 7 | using System.ComponentModel.DataAnnotations.Schema; 8 | using System.Linq; 9 | using Microsoft.EntityFrameworkCore; 10 | using Microsoft.VisualStudio.TestTools.UnitTesting; 11 | 12 | namespace MicroRuleEngine.Tests 13 | { 14 | [TestClass] 15 | public class InMemoryEntityFrameworkTests 16 | { 17 | private DbContextOptions options; 18 | 19 | [TestMethod] 20 | public void CheckSetup() 21 | { 22 | using (var context = new TestDbContext(options)) 23 | { 24 | var count = context.Students.Count(); 25 | 26 | Assert.AreEqual(8, 27 | count); 28 | } 29 | } 30 | 31 | [TestInitialize] 32 | public void Initialize() 33 | { 34 | options = new DbContextOptionsBuilder() 35 | .UseInMemoryDatabase(Guid.NewGuid() 36 | .ToString()) 37 | .Options; 38 | 39 | using (var context = new TestDbContext(options)) 40 | { 41 | context.Students.Add(new Student 42 | { 43 | FirstName = "Bob", 44 | LastName = "Smith", 45 | Gpa = 2.0m 46 | }); 47 | 48 | context.Students.Add(new Student 49 | { 50 | FirstName = "John", 51 | LastName = "Smith", 52 | Gpa = 3.5m 53 | }); 54 | 55 | context.Students.Add(new Student 56 | { 57 | FirstName = "Bob", 58 | LastName = "Jones", 59 | Gpa = 3.0m 60 | }); 61 | 62 | context.Students.Add(new Student 63 | { 64 | FirstName = "John", 65 | LastName = "Jones", 66 | Gpa = 4.0m 67 | }); 68 | 69 | context.Students.Add(new Student 70 | { 71 | FirstName = "Jane", 72 | LastName = "Smith", 73 | Gpa = 3.75m 74 | }); 75 | 76 | context.Students.Add(new Student 77 | { 78 | FirstName = "Sally", 79 | LastName = "Smith", 80 | Gpa = 1.0m 81 | }); 82 | 83 | context.Students.Add(new Student 84 | { 85 | FirstName = "Jane", 86 | LastName = "Jones", 87 | Gpa = 1.5m 88 | }); 89 | 90 | context.Students.Add(new Student 91 | { 92 | FirstName = "Sally", 93 | LastName = "Jones", 94 | Gpa = 2.5m 95 | }); 96 | 97 | context.SaveChanges(); 98 | } 99 | } 100 | 101 | [TestMethod] 102 | public void MoreComplicated() 103 | { 104 | var rule = 105 | new Rule 106 | { 107 | Operator = "OrElse", 108 | Rules = new List 109 | { 110 | new Rule 111 | { 112 | MemberName = "FirstName", 113 | Operator = "Equal", 114 | TargetValue = "Sally" 115 | }, 116 | new Rule 117 | { 118 | MemberName = "FirstName", 119 | Operator = "Equal", 120 | TargetValue = "Jane" 121 | } 122 | } 123 | }; 124 | 125 | var expression = MRE.ToExpression(rule, false); 126 | 127 | using (var context = new TestDbContext(options)) 128 | { 129 | var count = context.Students.Where(expression) 130 | .Count(); 131 | 132 | Assert.AreEqual(4, 133 | count); 134 | } 135 | } 136 | 137 | [TestMethod] 138 | public void ReallyComplicated() 139 | { 140 | var rule = 141 | new Rule 142 | { 143 | Operator = "AndAlso", 144 | Rules = new List 145 | { 146 | new Rule 147 | { 148 | Operator = "OrElse", 149 | Rules = new List 150 | { 151 | new Rule 152 | { 153 | MemberName = "FirstName", 154 | Operator = "Equal", 155 | TargetValue = "Sally" 156 | }, 157 | new Rule 158 | { 159 | MemberName = "FirstName", 160 | Operator = "Equal", 161 | TargetValue = "Jane" 162 | } 163 | } 164 | }, 165 | new Rule 166 | { 167 | MemberName = "Gpa", 168 | Operator = "GreaterThan", 169 | TargetValue = "2.0" 170 | } 171 | } 172 | }; 173 | 174 | var expression = MRE.ToExpression(rule, false); 175 | 176 | using (var context = new TestDbContext(options)) 177 | { 178 | var count = context.Students.Where(expression) 179 | .Count(); 180 | 181 | Assert.AreEqual(2, 182 | count); 183 | } 184 | } 185 | 186 | [TestMethod] 187 | public void SimpleRule() 188 | { 189 | var rule = new Rule 190 | { 191 | MemberName = "FirstName", 192 | Operator = "Equal", 193 | TargetValue = "Sally" 194 | }; 195 | 196 | var expression = MRE.ToExpression(rule, false); 197 | 198 | using (var context = new TestDbContext(options)) 199 | { 200 | var count = context.Students.Where(expression) 201 | .Count(); 202 | 203 | Assert.AreEqual(2, 204 | count); 205 | } 206 | } 207 | 208 | [TestMethod] 209 | [ExpectedException(typeof(NotImplementedException))] 210 | public void SimpleRule_IncludeTryCatch() 211 | { 212 | var rule = new Rule 213 | { 214 | MemberName = "FirstName", 215 | Operator = "Equal", 216 | TargetValue = "Sally" 217 | }; 218 | 219 | var expression = MRE.ToExpression(rule, true); 220 | 221 | using (var context = new TestDbContext(options)) 222 | { 223 | var count = context.Students.Where(expression) 224 | .Count(); 225 | 226 | Assert.AreEqual(2, 227 | count); 228 | } 229 | } 230 | } 231 | 232 | public class Student 233 | { 234 | [MaxLength(32)] 235 | public string FirstName { get; set; } 236 | 237 | public decimal Gpa { get; set; } 238 | 239 | [Key] 240 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 241 | public int Id { get; set; } 242 | 243 | [MaxLength(32)] 244 | public string LastName { get; set; } 245 | } 246 | 247 | public class TestDbContext : DbContext 248 | { 249 | public TestDbContext(DbContextOptions options) 250 | : base(options) 251 | { 252 | } 253 | 254 | public DbSet Students { get; set; } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /MicroRuleEngine.Core.Tests/MicroRuleEngine.Core.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /MicroRuleEngine.Core.Tests/Models/Blog.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using System.Collections.Generic; 3 | 4 | namespace EFModeling.Samples.DataSeeding 5 | { 6 | public class Blog 7 | { 8 | public int BlogId { get; set; } 9 | public string Url { get; set; } 10 | public virtual ICollection Posts { get; set; } 11 | } 12 | public class Post 13 | { 14 | public int PostId { get; set; } 15 | public string Content { get; set; } 16 | public string Title { get; set; } 17 | public int BlogId { get; set; } 18 | public Blog Blog { get; set; } 19 | public Name AuthorName { get; set; } 20 | } 21 | public class Name 22 | { 23 | public virtual string First { get; set; } 24 | public virtual string Last { get; set; } 25 | } 26 | public class BloggingContext : DbContext 27 | { 28 | public DbSet Blogs { get; set; } 29 | public DbSet Posts { get; set; } 30 | 31 | public BloggingContext(DbContextOptions options) 32 | : base(options) 33 | { } 34 | 35 | protected override void OnModelCreating(ModelBuilder modelBuilder) 36 | { 37 | modelBuilder.Entity(entity => 38 | { 39 | entity.Property(e => e.Url).IsRequired(); 40 | }); 41 | 42 | #region BlogSeed 43 | modelBuilder.Entity().HasData(new Blog { BlogId = 1, Url = "http://sample.com" }); 44 | #endregion 45 | 46 | modelBuilder.Entity(entity => 47 | { 48 | entity.HasOne(d => d.Blog) 49 | .WithMany(p => p.Posts) 50 | .HasForeignKey("BlogId"); 51 | }); 52 | 53 | #region PostSeed 54 | modelBuilder.Entity().HasData( 55 | new Post() { BlogId = 1, PostId = 1, Title = "First post", Content = "Test 1" }); 56 | #endregion 57 | 58 | #region AnonymousPostSeed 59 | modelBuilder.Entity().HasData( 60 | new { BlogId = 1, PostId = 2, Title = "Second post", Content = "Test 2" }); 61 | #endregion 62 | 63 | #region OwnedTypeSeed 64 | modelBuilder.Entity().OwnsOne(p => p.AuthorName).HasData( 65 | new { PostId = 1, First = "Andriy", Last = "Svyryd" }, 66 | new { PostId = 2, First = "Diego", Last = "Vega" }); 67 | #endregion 68 | } 69 | } 70 | } 71 | 72 | 73 | -------------------------------------------------------------------------------- /MicroRuleEngine.Core.Tests/Models/Order.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace MicroRuleEngine.Core.Tests.Models 6 | { 7 | public enum Status 8 | { 9 | Open, 10 | Cancelled, 11 | Completed 12 | }; 13 | 14 | public class Order 15 | { 16 | public Order() 17 | { 18 | Items = new List(); 19 | } 20 | public int OrderId { get; set; } 21 | public Customer Customer { get; set; } 22 | public List Items { get; set; } 23 | 24 | public List Codes { get; set; } 25 | public decimal? Total { get; set; } 26 | public DateTime OrderDate { get; set; } 27 | public bool HasItem(string itemCode) 28 | { 29 | return Items.Any(x => x.ItemCode == itemCode); 30 | } 31 | 32 | public Status Status { get; set; } 33 | 34 | } 35 | 36 | public class Item 37 | { 38 | public decimal Cost { get; set; } 39 | public string ItemCode { get; set; } 40 | } 41 | 42 | public class Customer 43 | { 44 | public string FirstName { get; set; } 45 | public string LastName { get; set; } 46 | public Country Country { get; set; } 47 | } 48 | 49 | public class Country 50 | { 51 | public string CountryCode { get; set; } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /MicroRuleEngine.Tests/DataRowTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System.Data; 4 | 5 | namespace MicroRuleEngine.Tests 6 | { 7 | [TestClass] 8 | public class DataRowTests 9 | { 10 | [TestMethod] 11 | public void DataRowTest() 12 | { 13 | var dr = GetDataRow(); 14 | // (int) dr["Column2"] == 123 && (string) dr["Column1"] == "Test" 15 | Rule rule = DataRule.Create("Column2", mreOperator.Equal, "123") & DataRule.Create("Column1", mreOperator.Equal, "Test"); 16 | 17 | MRE engine = new MRE(); 18 | var c1_123 = engine.CompileRule(rule); 19 | bool passes = c1_123(dr); 20 | Assert.IsTrue(passes); 21 | 22 | dr["Column2"] = 456; 23 | dr["Column1"] = "Hello"; 24 | passes = c1_123(dr); 25 | Assert.IsFalse(passes); 26 | } 27 | 28 | 29 | 30 | [TestMethod] 31 | public void DataRowTest_RuntimeType() 32 | { 33 | var dt = CreateEmptyDataTable(); 34 | 35 | Rule rule = DataRule.Create("Column2", mreOperator.Equal, 123, dt.Columns["Column2"].DataType); 36 | 37 | MRE engine = new MRE(); 38 | var c1_123 = engine.CompileRule(rule); 39 | 40 | var dr = GetDataRow(dt); 41 | bool passes = c1_123(dr); 42 | Assert.IsTrue(passes); 43 | 44 | dr["Column2"] = 456; 45 | dr["Column1"] = "Hello"; 46 | passes = c1_123(dr); 47 | Assert.IsFalse(passes); 48 | } 49 | 50 | [TestMethod] 51 | public void DataRowTest_DBNull() 52 | { 53 | Rule rule = DataRule.Create("Column2", mreOperator.Equal, (int?) null) & 54 | DataRule.Create("Column1", mreOperator.Equal, null); 55 | 56 | MRE engine = new MRE(); 57 | var c1_123 = engine.CompileRule(rule); 58 | 59 | var dt = CreateEmptyDataTable(); 60 | var dr = GetDataRowDBNull(dt); 61 | 62 | bool passes = c1_123(dr); 63 | Assert.IsTrue(passes); 64 | 65 | dr["Column2"] = 456; 66 | dr["Column1"] = "Hello"; 67 | passes = c1_123(dr); 68 | Assert.IsFalse(passes); 69 | } 70 | 71 | 72 | [TestMethod] 73 | public void DataRowTest_OldSytntax() 74 | { 75 | var dr = GetDataRow(); 76 | 77 | Rule rule = new DataRule 78 | { 79 | Type = "System.Int32", 80 | TargetValue = "123", 81 | Operator = "Equal", 82 | MemberName = "Column2" 83 | }; 84 | 85 | MRE engine = new MRE(); 86 | var c1_123 = engine.CompileRule(rule); 87 | bool passes = c1_123(dr); 88 | Assert.IsTrue(passes); 89 | 90 | dr["Column2"] = 456; 91 | dr["Column1"] = "Hello"; 92 | passes = c1_123(dr); 93 | Assert.IsFalse(passes); 94 | } 95 | 96 | DataTable CreateEmptyDataTable() 97 | { 98 | var dt = new DataTable(); 99 | dt.Columns.Add("Column1", typeof(string)); 100 | dt.Columns.Add("Column2", typeof(int)); 101 | dt.Columns[0].AllowDBNull = true; 102 | return dt; 103 | } 104 | 105 | DataRow GetDataRow(DataTable dt = null) 106 | { 107 | dt = dt ?? CreateEmptyDataTable(); 108 | var dr = dt.NewRow(); 109 | dr.ItemArray = new object[] {"Test", 123}; 110 | 111 | return dr; 112 | } 113 | 114 | DataRow GetDataRowDBNull(DataTable dt = null) 115 | { 116 | dt = dt ?? CreateEmptyDataTable(); 117 | var dr = dt.NewRow(); 118 | dr.ItemArray = new object[] { DBNull.Value, DBNull.Value }; 119 | 120 | return dr; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /MicroRuleEngine.Tests/ExampleUsage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using MicroRuleEngine.Tests.Models; 7 | 8 | namespace MicroRuleEngine.Tests 9 | { 10 | /// 11 | /// Summary description for UnitTest1 12 | /// 13 | [TestClass] 14 | public class ExampleUsage 15 | { 16 | [TestMethod] 17 | public void ChildPropertiesOfNull() 18 | { 19 | Order order = GetOrder(); 20 | order.Customer = null; 21 | Rule rule = new Rule 22 | { 23 | MemberName = "Customer.Country.CountryCode", 24 | Operator = ExpressionType.Equal.ToString("g"), 25 | TargetValue = "AUS" 26 | }; 27 | MRE engine = new MRE(); 28 | var compiledRule = engine.CompileRule(rule); 29 | bool passes = compiledRule(order); 30 | Assert.IsFalse(passes); 31 | } 32 | 33 | [TestMethod] 34 | public void ChildProperties() 35 | { 36 | Order order = GetOrder(); 37 | Rule rule = new Rule 38 | { 39 | MemberName = "Customer.Country.CountryCode", 40 | Operator = ExpressionType.Equal.ToString("g"), 41 | TargetValue = "AUS" 42 | }; 43 | MRE engine = new MRE(); 44 | var compiledRule = engine.CompileRule(rule); 45 | bool passes = compiledRule(order); 46 | Assert.IsTrue(passes); 47 | 48 | order.Customer.Country.CountryCode = "USA"; 49 | passes = compiledRule(order); 50 | Assert.IsFalse(passes); 51 | } 52 | 53 | [TestMethod] 54 | 55 | public void ConditionalLogic() 56 | { 57 | Order order = GetOrder(); 58 | Rule rule = new Rule 59 | { 60 | Operator = ExpressionType.AndAlso.ToString("g"), 61 | Rules = new List 62 | { 63 | new Rule { MemberName = "Customer.LastName", TargetValue = "Doe", Operator = "Equal"}, 64 | new Rule 65 | { 66 | Operator = "Or", 67 | Rules = new List 68 | { 69 | new Rule { MemberName = "Customer.FirstName", TargetValue = "John", Operator = "Equal"}, 70 | new Rule { MemberName = "Customer.FirstName", TargetValue = "Jane", Operator = "Equal"} 71 | } 72 | } 73 | } 74 | }; 75 | MRE engine = new MRE(); 76 | var fakeName = engine.CompileRule(rule); 77 | bool passes = fakeName(order); 78 | Assert.IsTrue(passes); 79 | 80 | order.Customer.FirstName = "Philip"; 81 | passes = fakeName(order); 82 | Assert.IsFalse(passes); 83 | } 84 | 85 | [TestMethod] 86 | public void BooleanMethods() 87 | { 88 | Order order = GetOrder(); 89 | Rule rule = new Rule 90 | { 91 | Operator = "HasItem",//The Order Object Contains a method named 'HasItem' that returns true/false 92 | Inputs = new List { "Test" } 93 | }; 94 | MRE engine = new MRE(); 95 | 96 | var boolMethod = engine.CompileRule(rule); 97 | bool passes = boolMethod(order); 98 | Assert.IsTrue(passes); 99 | 100 | var item = order.Items.First(x => x.ItemCode == "Test"); 101 | item.ItemCode = "Changed"; 102 | passes = boolMethod(order); 103 | Assert.IsFalse(passes); 104 | } 105 | 106 | [TestMethod] 107 | public void BooleanMethods_ByType() 108 | { 109 | Order order = GetOrder(); 110 | Rule rule = new Rule 111 | { 112 | Operator = "HasItem",//The Order Object Contains a method named 'HasItem' that returns true/false 113 | Inputs = new List { "Test" } 114 | }; 115 | MRE engine = new MRE(); 116 | 117 | var boolMethod = engine.CompileRule(typeof(Order), rule); 118 | bool passes =(bool) boolMethod.DynamicInvoke(order); 119 | Assert.IsTrue(passes); 120 | 121 | var item = order.Items.First(x => x.ItemCode == "Test"); 122 | item.ItemCode = "Changed"; 123 | passes = (bool)boolMethod.DynamicInvoke(order); 124 | Assert.IsFalse(passes); 125 | } 126 | 127 | [TestMethod] 128 | public void AnyOperator() 129 | { 130 | Order order = GetOrder(); 131 | //order.Items.Any(a => a.ItemCode == "test") 132 | Rule rule = new Rule 133 | { 134 | MemberName = "Items",// The array property 135 | Operator = "Any", 136 | Rules = new[] 137 | { 138 | new Rule 139 | { 140 | MemberName = "ItemCode", // the property in the above array item 141 | Operator = "Equal", 142 | TargetValue = "Test", 143 | } 144 | } 145 | }; 146 | MRE engine = new MRE(); 147 | var boolMethod = engine.CompileRule(rule); 148 | bool passes = boolMethod(order); 149 | Assert.IsTrue(passes); 150 | 151 | var item = order.Items.First(x => x.ItemCode == "Test"); 152 | item.ItemCode = "Changed"; 153 | passes = boolMethod(order); 154 | Assert.IsFalse(passes); 155 | } 156 | 157 | 158 | 159 | [TestMethod] 160 | public void ChildPropertyBooleanMethods() 161 | { 162 | Order order = GetOrder(); 163 | Rule rule = new Rule 164 | { 165 | MemberName = "Customer.FirstName", 166 | Operator = "EndsWith",//Regular method that exists on string.. As a note expression methods are not available 167 | Inputs = new List { "ohn" } 168 | }; 169 | MRE engine = new MRE(); 170 | var childPropCheck = engine.CompileRule(rule); 171 | bool passes = childPropCheck(order); 172 | Assert.IsTrue(passes); 173 | 174 | order.Customer.FirstName = "jane"; 175 | passes = childPropCheck(order); 176 | Assert.IsFalse(passes); 177 | } 178 | 179 | [TestMethod] 180 | public void ChildPropertyOfNullBooleanMethods() 181 | { 182 | Order order = GetOrder(); 183 | order.Customer = null; 184 | Rule rule = new Rule 185 | { 186 | MemberName = "Customer.FirstName", 187 | Operator = "EndsWith", //Regular method that exists on string.. As a note expression methods are not available 188 | Inputs = new List { "ohn" } 189 | }; 190 | MRE engine = new MRE(); 191 | var childPropCheck = engine.CompileRule(rule); 192 | bool passes = childPropCheck(order); 193 | Assert.IsFalse(passes); 194 | } 195 | [TestMethod] 196 | public void RegexIsMatch()//Had to add a Regex evaluator to make it feel 'Complete' 197 | { 198 | Order order = GetOrder(); 199 | Rule rule = new Rule 200 | { 201 | MemberName = "Customer.FirstName", 202 | Operator = "IsMatch", 203 | TargetValue = @"^[a-zA-Z0-9]*$" 204 | }; 205 | MRE engine = new MRE(); 206 | var regexCheck = engine.CompileRule(rule); 207 | bool passes = regexCheck(order); 208 | Assert.IsTrue(passes); 209 | 210 | order.Customer.FirstName = "--NoName"; 211 | passes = regexCheck(order); 212 | Assert.IsFalse(passes); 213 | } 214 | 215 | [TestMethod] 216 | public void BareString() 217 | { 218 | var rule = new Rule() 219 | { 220 | Operator = "StartsWith", 221 | Inputs = new[] { "FDX" } 222 | }; 223 | 224 | var engine = new MRE(); 225 | var childPropCheck = engine.CompileRule(rule); 226 | var passes = childPropCheck("FDX 123456"); 227 | Assert.IsTrue(passes); 228 | 229 | 230 | passes = childPropCheck("BOB 123456"); 231 | Assert.IsFalse(passes); 232 | } 233 | 234 | [TestMethod] 235 | public void IsInInput_SingleValue() 236 | { 237 | var value = "hello"; 238 | 239 | var rule = new Rule() 240 | { 241 | Operator = "IsInInput", 242 | Inputs = new List { "hello" } 243 | }; 244 | 245 | var mre = new MRE(); 246 | 247 | var ruleFunc = mre.CompileRule(rule); 248 | 249 | Assert.IsTrue(ruleFunc(value)); 250 | } 251 | 252 | [TestMethod] 253 | public void IsInInput_MultiValue() 254 | { 255 | var value = "hello"; 256 | 257 | var rule = new Rule() 258 | { 259 | Operator = "IsInInput", 260 | Inputs = new List { "hello", "World" } 261 | }; 262 | 263 | var mre = new MRE(); 264 | 265 | var ruleFunc = mre.CompileRule(rule); 266 | 267 | Assert.IsTrue(ruleFunc(value)); 268 | } 269 | 270 | [TestMethod] 271 | public void IsInInput_NoExactMatch() 272 | { 273 | var value = "world"; 274 | 275 | var rule = new Rule() 276 | { 277 | Operator = "IsInInput", 278 | Inputs = new List { "hello", "World" } 279 | }; 280 | 281 | var mre = new MRE(); 282 | 283 | var ruleFunc = mre.CompileRule(rule); 284 | 285 | Assert.IsFalse(ruleFunc(value)); 286 | } 287 | 288 | [TestMethod] 289 | public void MemberEqualsMember() 290 | { 291 | var testObj = new MemberOperaterMemberTestObject() 292 | { 293 | Source = "bob", 294 | Target = "bob" 295 | }; 296 | 297 | var rule = new Rule 298 | { 299 | MemberName = "Source", 300 | Operator = "Equal", 301 | TargetValue = "*.Target" 302 | }; 303 | 304 | var mre = new MRE(); 305 | 306 | var func = mre.CompileRule(rule); 307 | 308 | Assert.IsTrue(func(testObj)); 309 | 310 | testObj.Target = "notBob"; 311 | 312 | Assert.IsFalse(func(testObj)); 313 | } 314 | 315 | public static Order GetOrder() 316 | { 317 | Order order = new Order() 318 | { 319 | OrderId = 1, 320 | Customer = new Customer() 321 | { 322 | FirstName = "John", 323 | LastName = "Doe", 324 | Country = new Country() 325 | { 326 | CountryCode = "AUS" 327 | } 328 | }, 329 | Items = new List() 330 | { 331 | new Item { ItemCode = "MM23", Cost=5.25M}, 332 | new Item { ItemCode = "LD45", Cost=5.25M}, 333 | new Item { ItemCode = "Test", Cost=3.33M}, 334 | }, 335 | Codes = new List() 336 | { 337 | 555, 338 | 321, 339 | 243 340 | }, 341 | Total = 13.83m, 342 | OrderDate = new DateTime(1776, 7, 4), 343 | Status = Status.Open 344 | 345 | }; 346 | return order; 347 | } 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /MicroRuleEngine.Tests/ExceptionTests.cs: -------------------------------------------------------------------------------- 1 | using MicroRuleEngine.Tests.Models; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | namespace MicroRuleEngine.Tests 5 | { 6 | [TestClass] 7 | public class ExceptionTests 8 | { 9 | [TestMethod] 10 | [ExpectedException(typeof(RulesException))] 11 | public void BadPropertyName() 12 | { 13 | Order order = ExampleUsage.GetOrder(); 14 | Rule rule = Rule.Create("NotAProperty", mreOperator.Equal, 1); 15 | 16 | MRE engine = new MRE(); 17 | var compiledRule = engine.CompileRule(rule); 18 | bool passes = compiledRule(order); 19 | Assert.IsTrue(false); // should not get here. 20 | } 21 | 22 | [TestMethod] 23 | [ExpectedException(typeof(RulesException))] 24 | public void BadMethod() 25 | { 26 | Order order = ExampleUsage.GetOrder(); 27 | Rule rule = Rule.MethodOnChild("Customer.FirstName", "NotAMethod", "ohn"); 28 | 29 | MRE engine = new MRE(); 30 | var c1_123 = engine.CompileRule(rule); 31 | bool passes = c1_123(order); 32 | Assert.IsTrue(false); // should not get here. 33 | } 34 | 35 | [TestMethod] 36 | [ExpectedException(typeof(RulesException))] 37 | public void NotADataRow() 38 | { 39 | Order order = ExampleUsage.GetOrder(); 40 | Rule rule = DataRule.Create("Customer", mreOperator.Equal, "123"); 41 | 42 | MRE engine = new MRE(); 43 | var c1_123 = engine.CompileRule(rule); 44 | bool passes = c1_123(order); 45 | Assert.IsTrue(false); // should not get here. 46 | } 47 | 48 | [TestMethod] 49 | [ExpectedException(typeof(RulesException))] 50 | public void NotACollection() 51 | { 52 | Order order = ExampleUsage.GetOrder(); 53 | Rule rule = Rule.Create("Customer[1]", mreOperator.Equal, "123"); 54 | 55 | MRE engine = new MRE(); 56 | var c1_123 = engine.CompileRule(rule); 57 | bool passes = c1_123(order); 58 | Assert.IsTrue(false); // should not get here. 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /MicroRuleEngine.Tests/IsTypeTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | namespace MicroRuleEngine.Tests 4 | { 5 | [TestClass] 6 | public class IsTypeTests 7 | { 8 | [TestMethod] 9 | public void IsInt_OK() 10 | { 11 | var target = new IsTypeClass { NumAsString = "1234", OtherField = "Hello, World" }; 12 | 13 | Rule rule = Rule.IsInteger("NumAsString"); 14 | MRE engine = new MRE(); 15 | var compiledRule = engine.CompileRule(rule); 16 | bool passes = compiledRule(target); 17 | Assert.IsTrue(passes); 18 | } 19 | 20 | [TestMethod] 21 | public void IsInt_Bad_Float() 22 | { 23 | var target = new IsTypeClass { NumAsString = "1234.567", OtherField = "Hello, World" }; 24 | 25 | Rule rule = Rule.IsInteger("NumAsString"); 26 | MRE engine = new MRE(); 27 | var compiledRule = engine.CompileRule(rule); 28 | bool passes = compiledRule(target); 29 | Assert.IsFalse(passes); 30 | } 31 | 32 | [TestMethod] 33 | public void IsInt_Bad_Word() 34 | { 35 | var target = new IsTypeClass { NumAsString = "1234.567", OtherField = "Hello, World" }; 36 | 37 | Rule rule = Rule.IsInteger("OtherField"); 38 | MRE engine = new MRE(); 39 | var compiledRule = engine.CompileRule(rule); 40 | bool passes = compiledRule(target); 41 | Assert.IsFalse(passes); 42 | } 43 | [TestMethod] 44 | public void IsFloat_OK_Int() 45 | { 46 | var target = new IsTypeClass { NumAsString = "1234", OtherField = "Hello, World" }; 47 | 48 | Rule rule = Rule.IsFloat("NumAsString"); 49 | MRE engine = new MRE(); 50 | var compiledRule = engine.CompileRule(rule); 51 | bool passes = compiledRule(target); 52 | Assert.IsTrue(passes); 53 | } 54 | 55 | [TestMethod] 56 | public void IsFloat_OK_Float() 57 | { 58 | var target = new IsTypeClass { NumAsString = "1234.567", OtherField = "Hello, World" }; 59 | 60 | Rule rule = Rule.IsFloat("NumAsString"); 61 | MRE engine = new MRE(); 62 | var compiledRule = engine.CompileRule(rule); 63 | bool passes = compiledRule(target); 64 | Assert.IsTrue(passes); 65 | } 66 | 67 | [TestMethod] 68 | public void IsFloat_Bad_Word() 69 | { 70 | var target = new IsTypeClass { NumAsString = "1234.567", OtherField = "Hello, World" }; 71 | 72 | Rule rule = Rule.IsFloat("OtherField"); 73 | MRE engine = new MRE(); 74 | var compiledRule = engine.CompileRule(rule); 75 | bool passes = compiledRule(target); 76 | Assert.IsFalse(passes); 77 | } 78 | 79 | [TestMethod] 80 | public void IsDouble_OK_Int() 81 | { 82 | var target = new IsTypeClass { NumAsString = "1234", OtherField = "Hello, World" }; 83 | 84 | Rule rule = Rule.IsDouble("NumAsString"); 85 | MRE engine = new MRE(); 86 | var compiledRule = engine.CompileRule(rule); 87 | bool passes = compiledRule(target); 88 | Assert.IsTrue(passes); 89 | } 90 | 91 | [TestMethod] 92 | public void IsDouble_OK_Double() 93 | { 94 | var target = new IsTypeClass { NumAsString = "1234.56789012345", OtherField = "Hello, World" }; 95 | 96 | Rule rule = Rule.IsDouble("NumAsString"); 97 | MRE engine = new MRE(); 98 | var compiledRule = engine.CompileRule(rule); 99 | bool passes = compiledRule(target); 100 | Assert.IsTrue(passes); 101 | } 102 | 103 | [TestMethod] 104 | public void IsDouble_Bad_Word() 105 | { 106 | var target = new IsTypeClass { NumAsString = "1234.567", OtherField = "Hello, World" }; 107 | 108 | Rule rule = Rule.IsDouble("OtherField"); 109 | MRE engine = new MRE(); 110 | var compiledRule = engine.CompileRule(rule); 111 | bool passes = compiledRule(target); 112 | Assert.IsFalse(passes); 113 | } 114 | 115 | [TestMethod] 116 | public void IsDecimal_OK_Int() 117 | { 118 | var target = new IsTypeClass { NumAsString = "1234", OtherField = "Hello, World" }; 119 | 120 | Rule rule = Rule.IsDecimal("NumAsString"); 121 | MRE engine = new MRE(); 122 | var compiledRule = engine.CompileRule(rule); 123 | bool passes = compiledRule(target); 124 | Assert.IsTrue(passes); 125 | } 126 | 127 | [TestMethod] 128 | public void IsDecimal_OK_Double() 129 | { 130 | var target = new IsTypeClass { NumAsString = "1234.56789012345", OtherField = "Hello, World" }; 131 | 132 | Rule rule = Rule.IsDecimal("NumAsString"); 133 | MRE engine = new MRE(); 134 | var compiledRule = engine.CompileRule(rule); 135 | bool passes = compiledRule(target); 136 | Assert.IsTrue(passes); 137 | } 138 | 139 | [TestMethod] 140 | public void IsDecimal_Bad_Word() 141 | { 142 | var target = new IsTypeClass { NumAsString = "1234.567", OtherField = "Hello, World" }; 143 | 144 | Rule rule = Rule.IsDecimal("OtherField"); 145 | MRE engine = new MRE(); 146 | var compiledRule = engine.CompileRule(rule); 147 | bool passes = compiledRule(target); 148 | Assert.IsFalse(passes); 149 | } 150 | 151 | 152 | } 153 | 154 | internal class IsTypeClass 155 | { 156 | public string NumAsString { get; set; } 157 | public string OtherField { get; set; } 158 | } 159 | } -------------------------------------------------------------------------------- /MicroRuleEngine.Tests/MicroRuleEngine.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | 7 | 8 | 2.0 9 | {392E9585-525F-4B82-9DA0-C4AC2812C558} 10 | Library 11 | Properties 12 | MicroRuleEngine.Tests 13 | MicroRuleEngine.Tests 14 | v4.6.1 15 | 512 16 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 17 | 18 | 19 | 20 | true 21 | full 22 | false 23 | bin\Debug\ 24 | DEBUG;TRACE 25 | prompt 26 | 4 27 | false 28 | 29 | 30 | pdbonly 31 | true 32 | bin\Release\ 33 | TRACE 34 | prompt 35 | 4 36 | false 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 3.5 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | False 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | {c6f8fbce-8949-455a-ae4a-20a2b2012aee} 72 | MicroRuleEngine 73 | 74 | 75 | 76 | 83 | -------------------------------------------------------------------------------- /MicroRuleEngine.Tests/Models/MemberOperaterMemberTestObject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace MicroRuleEngine.Tests.Models 8 | { 9 | class MemberOperaterMemberTestObject 10 | { 11 | public string Source { get; set; } 12 | public string Target { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MicroRuleEngine.Tests/Models/Order.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace MicroRuleEngine.Tests.Models 6 | { 7 | public enum Status 8 | { 9 | Open, 10 | Cancelled, 11 | Completed 12 | }; 13 | 14 | public class Order 15 | { 16 | public Order() 17 | { 18 | Items = new List(); 19 | } 20 | public int OrderId { get; set; } 21 | public Customer Customer { get; set; } 22 | public List Items { get; set; } 23 | public decimal? Total { get; set; } 24 | public DateTime OrderDate { get; set; } 25 | public bool HasItem(string itemCode) 26 | { 27 | return Items.Any(x => x.ItemCode == itemCode); 28 | } 29 | 30 | public Status Status { get; set; } 31 | public List Codes { get; set; } 32 | } 33 | 34 | public class Item 35 | { 36 | public decimal Cost { get; set; } 37 | public string ItemCode { get; set; } 38 | } 39 | 40 | public class Customer 41 | { 42 | public string FirstName { get; set; } 43 | public string LastName { get; set; } 44 | public Country Country { get; set; } 45 | } 46 | 47 | public class Country 48 | { 49 | public string CountryCode { get; set; } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /MicroRuleEngine.Tests/NewAPI.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using MicroRuleEngine.Tests.Models; 6 | 7 | namespace MicroRuleEngine.Tests 8 | { 9 | /// 10 | /// Summary description for UnitTest1 11 | /// 12 | [TestClass] 13 | public class NewApi 14 | { 15 | [TestMethod] 16 | public void ChildProperties2() 17 | { 18 | Order order = ExampleUsage.GetOrder(); 19 | Rule rule = Rule.Create("Customer.Country.CountryCode", mreOperator.Equal, "AUS"); 20 | 21 | MRE engine = new MRE(); 22 | var compiledRule = engine.CompileRule(rule); 23 | bool passes = compiledRule(order); 24 | Assert.IsTrue(passes); 25 | 26 | order.Customer.Country.CountryCode = "USA"; 27 | passes = compiledRule(order); 28 | Assert.IsFalse(passes); 29 | } 30 | 31 | [TestMethod] 32 | public void IntProperties() 33 | { 34 | Order order = ExampleUsage.GetOrder(); 35 | Rule rule = Rule.Create("OrderId", mreOperator.Equal, 1); 36 | 37 | MRE engine = new MRE(); 38 | var compiledRule = engine.CompileRule(rule); 39 | bool passes = compiledRule(order); 40 | Assert.IsTrue(passes); 41 | 42 | order.OrderId = 5; 43 | passes = compiledRule(order); 44 | Assert.IsFalse(passes); 45 | } 46 | 47 | 48 | [TestMethod] 49 | public void DateProperties() 50 | { 51 | Order order = ExampleUsage.GetOrder(); 52 | Rule rule = Rule.Create("OrderDate", mreOperator.LessThan, "1800-01-01"); 53 | 54 | MRE engine = new MRE(); 55 | var compiledRule = engine.CompileRule(rule); 56 | bool passes = compiledRule(order); 57 | Assert.IsTrue(passes); 58 | 59 | order.OrderDate = new DateTime(1814, 9, 13); 60 | passes = compiledRule(order); 61 | Assert.IsFalse(passes); 62 | } 63 | 64 | [TestMethod] 65 | public void DecimalProperties() 66 | { 67 | Order order = ExampleUsage.GetOrder(); 68 | Rule rule = Rule.Create("Total", mreOperator.GreaterThan, 12.00m); 69 | 70 | MRE engine = new MRE(); 71 | var compiledRule = engine.CompileRule(rule); 72 | bool passes = compiledRule(order); 73 | Assert.IsTrue(passes); 74 | 75 | order.Total = 9.99m; 76 | passes = compiledRule(order); 77 | Assert.IsFalse(passes); 78 | } 79 | 80 | [TestMethod] 81 | public void NullableProperties() 82 | { 83 | Order order = ExampleUsage.GetOrder(); 84 | order.Total = null; 85 | Rule rule = Rule.Create("Total", mreOperator.Equal, null); 86 | 87 | MRE engine = new MRE(); 88 | var compiledRule = engine.CompileRule(rule); 89 | bool passes = compiledRule(order); 90 | Assert.IsTrue(passes); 91 | 92 | order.Total = 9.99m; 93 | passes = compiledRule(order); 94 | Assert.IsFalse(passes); 95 | } 96 | 97 | [TestMethod] 98 | public void NullAsWord() 99 | { 100 | Order order = ExampleUsage.GetOrder(); 101 | order.Total = null; 102 | Rule rule = Rule.Create("Total", mreOperator.Equal, "null"); 103 | 104 | MRE engine = new MRE(); 105 | var compiledRule = engine.CompileRule(rule); 106 | bool passes = compiledRule(order); 107 | Assert.IsTrue(passes); 108 | 109 | order.Total = 9.99m; 110 | passes = compiledRule(order); 111 | Assert.IsFalse(passes); 112 | } 113 | 114 | [TestMethod] 115 | public void EnumProperties() 116 | { 117 | Order order = ExampleUsage.GetOrder(); 118 | order.Total = null; 119 | Rule rule = Rule.Create("Status", mreOperator.Equal, Status.Open); 120 | 121 | MRE engine = new MRE(); 122 | var compiledRule = engine.CompileRule(rule); 123 | bool passes = compiledRule(order); 124 | Assert.IsTrue(passes); 125 | 126 | } 127 | [TestMethod] 128 | public void EnumAsWord() 129 | { 130 | Order order = ExampleUsage.GetOrder(); 131 | order.Total = null; 132 | Rule rule = Rule.Create("Status", mreOperator.Equal, "Open"); 133 | 134 | MRE engine = new MRE(); 135 | var compiledRule = engine.CompileRule(rule); 136 | bool passes = compiledRule(order); 137 | Assert.IsTrue(passes); 138 | 139 | } 140 | 141 | 142 | [TestMethod] 143 | public void ArrayTest() 144 | { 145 | var array = new ArrayInside(); 146 | 147 | Rule rule = Rule.Create("Dbl[1]", mreOperator.Equal, 22.222); 148 | 149 | MRE engine = new MRE(); 150 | var compiledRule = engine.CompileRule(rule); 151 | bool passes = compiledRule(array); 152 | Assert.IsTrue(passes); 153 | 154 | array.Dbl[1] = .0001; 155 | passes = compiledRule(array); 156 | Assert.IsFalse(passes); 157 | } 158 | 159 | class ArrayInside 160 | { 161 | public double[] Dbl { get; }= {1.111, 22.222, 333.333}; 162 | } 163 | 164 | [TestMethod] 165 | public void ListTest() 166 | { 167 | Order order = ExampleUsage.GetOrder(); 168 | 169 | Rule rule = Rule.Create("Items[1].Cost", mreOperator.Equal, 5.25m); 170 | 171 | MRE engine = new MRE(); 172 | var compiledRule = engine.CompileRule(rule); 173 | bool passes = compiledRule(order); 174 | Assert.IsTrue(passes); 175 | 176 | order.Items[1].Cost = 6.99m; 177 | passes = compiledRule(order); 178 | Assert.IsFalse(passes); 179 | } 180 | 181 | class DictTest 182 | { 183 | public Dictionary Dict { get; set; } 184 | } 185 | 186 | [TestMethod] 187 | public void Dictionary_StringIndex() 188 | { 189 | var objDict = new DictTest { Dict = new Dictionary()}; 190 | objDict.Dict.Add("Key", 1234); 191 | 192 | Rule rule = Rule.Create("Dict['Key']", mreOperator.Equal, 1234); 193 | 194 | MRE engine = new MRE(); 195 | var compiledRule = engine.CompileRule>(rule); 196 | bool passes = compiledRule(objDict); 197 | Assert.IsTrue(passes); 198 | 199 | objDict.Dict["Key"] = 2345; 200 | passes = compiledRule(objDict); 201 | Assert.IsFalse(passes); 202 | } 203 | 204 | [TestMethod] 205 | public void Dictionary_IntIndex() 206 | { 207 | var objDict = new DictTest { Dict = new Dictionary() }; 208 | objDict.Dict.Add(111, 1234); 209 | 210 | Rule rule = Rule.Create("Dict[111]", mreOperator.Equal, 1234); 211 | 212 | MRE engine = new MRE(); 213 | var compiledRule = engine.CompileRule>(rule); 214 | bool passes = compiledRule(objDict); 215 | Assert.IsTrue(passes); 216 | 217 | objDict.Dict[111] = 2345; 218 | passes = compiledRule(objDict); 219 | Assert.IsFalse(passes); 220 | } 221 | 222 | [TestMethod] 223 | public void SelfReferenialTest() 224 | { 225 | Order order = ExampleUsage.GetOrder(); 226 | 227 | Rule rule = Rule.Create("Items[1].Cost", mreOperator.Equal, "*.Items[0].Cost"); 228 | 229 | MRE engine = new MRE(); 230 | var compiledRule = engine.CompileRule(rule); 231 | bool passes = compiledRule(order); 232 | Assert.IsTrue(passes); 233 | 234 | order.Items[1].Cost = 6.99m; 235 | passes = compiledRule(order); 236 | Assert.IsFalse(passes); 237 | } 238 | 239 | [TestMethod] 240 | public void ConditionalLogic2() 241 | { 242 | Order order = ExampleUsage.GetOrder(); 243 | Rule rule = Rule.Create("Customer.LastName", mreOperator.Equal, "Doe") 244 | & (Rule.Create("Customer.FirstName", mreOperator.Equal, "John") | Rule.Create("Customer.FirstName", mreOperator.Equal, "Jane")); 245 | 246 | MRE engine = new MRE(); 247 | var fakeName = engine.CompileRule(rule); 248 | bool passes = fakeName(order); 249 | Assert.IsTrue(passes); 250 | 251 | order.Customer.FirstName = "Philip"; 252 | passes = fakeName(order); 253 | Assert.IsFalse(passes); 254 | } 255 | 256 | [TestMethod] 257 | public void BooleanMethods2() 258 | { 259 | Order order = ExampleUsage.GetOrder(); 260 | 261 | //The Order Object Contains a method named 'HasItem(string itemCode)' that returns true/false 262 | Rule rule = Rule.Method("HasItem", "Test"); 263 | 264 | MRE engine = new MRE(); 265 | var boolMethod = engine.CompileRule(rule); 266 | bool passes = boolMethod(order); 267 | Assert.IsTrue(passes); 268 | 269 | var item = order.Items.First(x => x.ItemCode == "Test"); 270 | item.ItemCode = "Changed"; 271 | passes = boolMethod(order); 272 | Assert.IsFalse(passes); 273 | } 274 | 275 | [TestMethod] 276 | public void ChildPropertyBooleanMethods2() 277 | { 278 | Order order = ExampleUsage.GetOrder(); 279 | //Regular method that exists on string.. As a note expression methods are not available 280 | Rule rule = Rule.MethodOnChild("Customer.FirstName", "EndsWith", "ohn"); 281 | 282 | MRE engine = new MRE(); 283 | var childPropCheck = engine.CompileRule(rule); 284 | bool passes = childPropCheck(order); 285 | Assert.IsTrue(passes); 286 | 287 | order.Customer.FirstName = "jane"; 288 | passes = childPropCheck(order); 289 | Assert.IsFalse(passes); 290 | } 291 | 292 | [TestMethod] 293 | public void RegexIsMatch2()//Had to add a Regex evaluator to make it feel 'Complete' 294 | { 295 | Order order = ExampleUsage.GetOrder(); 296 | // Regex = Capital letter, vowel, then two constanants ("John" passes, "Jane" fails) 297 | Rule rule = Rule.Create("Customer.FirstName", mreOperator.IsMatch, @"^[A-Z][aeiou][bcdfghjklmnpqrstvwxyz]{2}$"); 298 | 299 | MRE engine = new MRE(); 300 | var regexCheck = engine.CompileRule(rule); 301 | bool passes = regexCheck(order); 302 | Assert.IsTrue(passes); 303 | 304 | order.Customer.FirstName = "Jane"; 305 | passes = regexCheck(order); 306 | Assert.IsFalse(passes); 307 | } 308 | 309 | [TestMethod] 310 | public void AnyOperator() 311 | { 312 | Order order = ExampleUsage.GetOrder(); 313 | //order.Items.Any(a => a.ItemCode == "test") 314 | Rule rule = Rule.Any("Items", Rule.Create("ItemCode", mreOperator.Equal, "Test")); 315 | 316 | MRE engine = new MRE(); 317 | var boolMethod = engine.CompileRule(rule); 318 | bool passes = boolMethod(order); 319 | Assert.IsTrue(passes); 320 | 321 | var item = order.Items.First(x => x.ItemCode == "Test"); 322 | item.ItemCode = "Changed"; 323 | passes = boolMethod(order); 324 | Assert.IsFalse(passes); 325 | } 326 | 327 | 328 | [TestMethod] 329 | public void AllOperator() 330 | { 331 | Order order = ExampleUsage.GetOrder(); 332 | //order.Items.All(a => a.Cost > 2.00m) 333 | Rule rule = Rule.All("Items", Rule.Create("Cost", mreOperator.GreaterThan, "2.00")); 334 | 335 | MRE engine = new MRE(); 336 | var boolMethod = engine.CompileRule(rule); 337 | bool passes = boolMethod(order); 338 | Assert.IsTrue(passes); 339 | 340 | var item = order.Items.First(x => x.ItemCode == "Test"); 341 | item.Cost = 1.99m; 342 | passes = boolMethod(order); 343 | Assert.IsFalse(passes); 344 | } 345 | 346 | [TestMethod] 347 | public void Prebuild2() 348 | { 349 | MRE engine = new MRE(); 350 | Rule rule1 = Rule.MethodOnChild("Customer.FirstName", "EndsWith", "ohn"); 351 | Rule rule2 = Rule.Create("Customer.Country.CountryCode", mreOperator.Equal, "AUS"); 352 | 353 | var endsWithOhn = engine.CompileRule(rule1); 354 | var inAus = engine.CompileRule(rule2); 355 | 356 | Order order = ExampleUsage.GetOrder(); 357 | 358 | int reps = 1000; 359 | for (int i = 0; i < reps; ++i) 360 | { 361 | bool passes = endsWithOhn(order); 362 | Assert.IsTrue(passes); 363 | 364 | passes = inAus(order); 365 | Assert.IsTrue(passes); 366 | } 367 | } 368 | 369 | 370 | 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /MicroRuleEngine.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("MicroRuleEngine.Tests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("MicroRuleEngine.Tests")] 13 | [assembly: AssemblyCopyright("Copyright © 2012")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("e713bef8-d7c9-45eb-903f-d9d8cf08b614")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /MicroRuleEngine.Tests/SerializationTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System.Runtime.Serialization; 4 | using System.IO; 5 | using MicroRuleEngine.Tests.Models; 6 | using System.Xml; 7 | using System.Runtime.Serialization.Json; 8 | using System.Text; 9 | 10 | namespace MicroRuleEngine.Tests 11 | { 12 | [TestClass] 13 | public class SerializationTests 14 | { 15 | [TestMethod] 16 | public void XmlSerialization() 17 | { 18 | var serializer = new DataContractSerializer(typeof(Rule)); 19 | string text; 20 | 21 | using (var writer = new StringWriter()) 22 | { 23 | Rule rule = Rule.Create("Customer.LastName", mreOperator.Equal, "Doe") 24 | & (Rule.Create("Customer.FirstName", mreOperator.Equal, "John") 25 | | Rule.Create("Customer.FirstName", mreOperator.Equal, "Jane")); 26 | 27 | using (var xw = XmlWriter.Create(writer)) 28 | serializer.WriteObject(xw, rule); 29 | text = writer.ToString(); 30 | } 31 | 32 | Rule newRule; // add breakpoint here, to view XML in text. 33 | 34 | using (var reader = new StringReader(text)) 35 | using (var xr = XmlReader.Create(reader)) 36 | { 37 | newRule = (Rule)serializer.ReadObject(xr); 38 | } 39 | 40 | var order = ExampleUsage.GetOrder(); 41 | 42 | MRE engine = new MRE(); 43 | var fakeName = engine.CompileRule(newRule); 44 | bool passes = fakeName(order); 45 | Assert.IsTrue(passes); 46 | 47 | order.Customer.FirstName = "Philip"; 48 | passes = fakeName(order); 49 | Assert.IsFalse(passes); 50 | } 51 | 52 | [TestMethod] 53 | public void JsonSerialization() 54 | { 55 | 56 | var serializer = new DataContractJsonSerializer(typeof(Rule)); 57 | 58 | string text; 59 | 60 | using (var stream1 = new MemoryStream()) 61 | { 62 | Rule rule = Rule.Create("Customer.LastName", mreOperator.Equal, "Doe") 63 | & (Rule.Create("Customer.FirstName", mreOperator.Equal, "John") | 64 | Rule.Create("Customer.FirstName", mreOperator.Equal, "Jane")) 65 | & Rule.Create("Items[1].Cost", mreOperator.Equal, 5.25m); 66 | 67 | serializer.WriteObject(stream1, rule); 68 | 69 | stream1.Position = 0; 70 | var sr = new StreamReader(stream1); 71 | text = sr.ReadToEnd(); 72 | } 73 | 74 | Rule newRule; // add breakpoint here, to view JSON in text. 75 | 76 | using (var stream2 = new MemoryStream(Encoding.UTF8.GetBytes(text))) 77 | { 78 | newRule = (Rule)serializer.ReadObject(stream2); 79 | } 80 | 81 | var order = ExampleUsage.GetOrder(); 82 | 83 | MRE engine = new MRE(); 84 | var fakeName = engine.CompileRule(newRule); 85 | bool passes = fakeName(order); 86 | Assert.IsTrue(passes); 87 | 88 | order.Customer.FirstName = "Philip"; 89 | passes = fakeName(order); 90 | Assert.IsFalse(passes); 91 | } 92 | 93 | [TestMethod] 94 | public void JsonVisualizationTest() 95 | { 96 | var order = ExampleUsage.GetOrder(); 97 | var members = MRE.Member.GetFields(order.GetType()); 98 | var serializer = new DataContractJsonSerializer(members.GetType()); 99 | 100 | string text; 101 | 102 | using (var stream1 = new MemoryStream()) 103 | { 104 | 105 | serializer.WriteObject(stream1, members); 106 | 107 | stream1.Position = 0; 108 | var sr = new StreamReader(stream1); 109 | text = sr.ReadToEnd(); 110 | } 111 | Assert.IsTrue(text.Length > 100); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /MicroRuleEngine.Tests/TimeTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MicroRuleEngine.Tests.Models; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace MicroRuleEngine.Tests 6 | { 7 | [TestClass] 8 | public class TimeTest 9 | { 10 | [TestMethod] 11 | public void Time_InRange_Minutes() 12 | { 13 | Order order = ExampleUsage.GetOrder(); 14 | Rule rule = Rule.Create("OrderDate", mreOperator.GreaterThanOrEqual, "#NOW-90M"); 15 | order.OrderDate = DateTime.Now.AddMinutes(-60); 16 | 17 | MRE engine = new MRE(); 18 | var boolMethod = engine.CompileRule(rule); 19 | bool passes = boolMethod(order); 20 | Assert.IsTrue(passes); 21 | 22 | } 23 | 24 | [TestMethod] 25 | public void Time_OutOfRange_Minutes() 26 | { 27 | Order order = ExampleUsage.GetOrder(); 28 | Rule rule = Rule.Create("OrderDate", mreOperator.GreaterThanOrEqual, "#NOW-90M"); 29 | order.OrderDate = DateTime.Now.AddMinutes(-100); 30 | 31 | MRE engine = new MRE(); 32 | var boolMethod = engine.CompileRule(rule); 33 | bool passes = boolMethod(order); 34 | Assert.IsFalse(passes); 35 | 36 | } 37 | 38 | [TestMethod] 39 | [ExpectedException(typeof(FormatException))] // TODO: Make throw RuleException 40 | public void Time_BadTarget() 41 | { 42 | Order order = ExampleUsage.GetOrder(); 43 | Rule rule = Rule.Create("OrderId", mreOperator.GreaterThanOrEqual, "#NOW-90M"); 44 | order.OrderDate = DateTime.Now.AddMinutes(-100); 45 | 46 | MRE engine = new MRE(); 47 | var boolMethod = engine.CompileRule(rule); 48 | bool passes = boolMethod(order); 49 | Assert.IsFalse(passes); 50 | } 51 | 52 | [TestMethod] 53 | [ExpectedException(typeof(FormatException))] // TODO: Make throw RuleException 54 | public void Time_BadDateString() 55 | { 56 | Order order = ExampleUsage.GetOrder(); 57 | Rule rule = Rule.Create("OrderDate", mreOperator.GreaterThanOrEqual, "#NOW*90M"); 58 | order.OrderDate = DateTime.Now.AddMinutes(-100); 59 | 60 | MRE engine = new MRE(); 61 | var boolMethod = engine.CompileRule(rule); 62 | bool passes = boolMethod(order); 63 | Assert.IsFalse(passes); 64 | 65 | } 66 | 67 | [TestMethod] 68 | public void Time_InRange_Days() 69 | { 70 | Order order = ExampleUsage.GetOrder(); 71 | Rule rule = Rule.Create("OrderDate", mreOperator.GreaterThanOrEqual, "#NOW-90D"); 72 | order.OrderDate = DateTime.Now.AddDays(-60); 73 | 74 | MRE engine = new MRE(); 75 | var boolMethod = engine.CompileRule(rule); 76 | bool passes = boolMethod(order); 77 | Assert.IsTrue(passes); 78 | } 79 | 80 | [TestMethod] 81 | public void Time_OutOfRange_Days() 82 | { 83 | Order order = ExampleUsage.GetOrder(); 84 | Rule rule = Rule.Create("OrderDate", mreOperator.GreaterThanOrEqual, "#NOW-90D"); 85 | order.OrderDate = DateTime.Now.AddDays(-100); 86 | 87 | MRE engine = new MRE(); 88 | var boolMethod = engine.CompileRule(rule); 89 | bool passes = boolMethod(order); 90 | Assert.IsFalse(passes); 91 | 92 | } 93 | 94 | 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /MicroRuleEngine.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29230.47 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroRuleEngine.Tests", "MicroRuleEngine.Tests\MicroRuleEngine.Tests.csproj", "{392E9585-525F-4B82-9DA0-C4AC2812C558}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2FBBE4F8-7AD1-4132-89D5-81035CC566BC}" 9 | ProjectSection(SolutionItems) = preProject 10 | Local.testsettings = Local.testsettings 11 | MicroRuleEngine.vsmdi = MicroRuleEngine.vsmdi 12 | README.md = README.md 13 | TraceAndTestImpact.testsettings = TraceAndTestImpact.testsettings 14 | EndProjectSection 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroRuleEngine", "MicroRuleEngine\MicroRuleEngine.csproj", "{C6F8FBCE-8949-455A-AE4A-20A2B2012AEE}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroRuleEngine.Core.Tests", "MicroRuleEngine.Core.Tests\MicroRuleEngine.Core.Tests.csproj", "{B070AE15-A3F8-4B33-9FC0-B2BB4C989A45}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {392E9585-525F-4B82-9DA0-C4AC2812C558}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {392E9585-525F-4B82-9DA0-C4AC2812C558}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {392E9585-525F-4B82-9DA0-C4AC2812C558}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {392E9585-525F-4B82-9DA0-C4AC2812C558}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {C6F8FBCE-8949-455A-AE4A-20A2B2012AEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {C6F8FBCE-8949-455A-AE4A-20A2B2012AEE}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {C6F8FBCE-8949-455A-AE4A-20A2B2012AEE}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {C6F8FBCE-8949-455A-AE4A-20A2B2012AEE}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {B070AE15-A3F8-4B33-9FC0-B2BB4C989A45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {B070AE15-A3F8-4B33-9FC0-B2BB4C989A45}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {B070AE15-A3F8-4B33-9FC0-B2BB4C989A45}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {B070AE15-A3F8-4B33-9FC0-B2BB4C989A45}.Release|Any CPU.Build.0 = Release|Any CPU 38 | EndGlobalSection 39 | GlobalSection(SolutionProperties) = preSolution 40 | HideSolutionNode = FALSE 41 | EndGlobalSection 42 | GlobalSection(ExtensibilityGlobals) = postSolution 43 | SolutionGuid = {B855DA2D-6F7A-4B9B-BF94-81DA13513977} 44 | EndGlobalSection 45 | GlobalSection(Performance) = preSolution 46 | HasPerformanceSessions = true 47 | EndGlobalSection 48 | GlobalSection(Performance) = preSolution 49 | HasPerformanceSessions = true 50 | EndGlobalSection 51 | GlobalSection(TestCaseManagementSettings) = postSolution 52 | CategoryFile = MicroRuleEngine.vsmdi 53 | EndGlobalSection 54 | EndGlobal 55 | -------------------------------------------------------------------------------- /MicroRuleEngine/MRE.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Linq.Expressions; 7 | using System.Reflection; 8 | using System.Runtime.Serialization; 9 | using System.Text.RegularExpressions; 10 | 11 | namespace MicroRuleEngine 12 | { 13 | public class MRE 14 | { 15 | private static readonly ExpressionType[] _nestedOperators = new ExpressionType[] 16 | {ExpressionType.And, ExpressionType.AndAlso, ExpressionType.Or, ExpressionType.OrElse}; 17 | 18 | private static readonly Lazy _miRegexIsMatch = new Lazy(() => 19 | typeof(Regex).GetMethod("IsMatch", new[] { typeof(string), typeof(string), typeof(RegexOptions) })); 20 | 21 | private static readonly Lazy _miGetItem = new Lazy(() => 22 | typeof(System.Data.DataRow).GetMethod("get_Item", new Type[] { typeof(string) })); 23 | 24 | private static readonly Lazy _miListContains = new Lazy(() => 25 | typeof(IList).GetMethod("Contains", new[] { typeof(object) })); 26 | 27 | private static readonly Tuple>[] _enumrMethodsByName = 28 | new Tuple>[] 29 | { 30 | Tuple.Create("Any", new Lazy(() => GetLinqMethod("Any", 2))), 31 | Tuple.Create("All", new Lazy(() => GetLinqMethod("All", 2))), 32 | }; 33 | private static readonly Lazy _miIntTryParse = new Lazy(() => 34 | typeof(Int32).GetMethod("TryParse", new Type[] { typeof(string), Type.GetType("System.Int32&") })); 35 | 36 | private static readonly Lazy _miFloatTryParse = new Lazy(() => 37 | typeof(Single).GetMethod("TryParse", new Type[] { typeof(string), Type.GetType("System.Single&") })); 38 | 39 | private static readonly Lazy _miDoubleTryParse = new Lazy(() => 40 | typeof(Double).GetMethod("TryParse", new Type[] { typeof(string), Type.GetType("System.Double&") })); 41 | 42 | private static readonly Lazy _miDecimalTryParse = new Lazy(() => 43 | typeof(Decimal).GetMethod("TryParse", new Type[] { typeof(string), Type.GetType("System.Decimal&") })); 44 | 45 | public Func CompileRule(Rule r) 46 | { 47 | var paramUser = Expression.Parameter(typeof(T)); 48 | Expression expr = GetExpressionForRule(typeof(T), r, paramUser); 49 | 50 | return Expression.Lambda>(expr, paramUser).Compile(); 51 | } 52 | public static Expression> ToExpression(Rule r, bool useTryCatchForNulls = true) 53 | { 54 | var paramUser = Expression.Parameter(typeof(T)); 55 | Expression expr = GetExpressionForRule(typeof(T), r, paramUser, useTryCatchForNulls); 56 | 57 | return Expression.Lambda>(expr, paramUser); 58 | } 59 | 60 | public static Func ToFunc(Rule r, bool useTryCatchForNulls = true) 61 | { 62 | return ToExpression(r, useTryCatchForNulls).Compile(); 63 | } 64 | public static Expression> ToExpression(Type type, Rule r) 65 | { 66 | var paramUser = Expression.Parameter(typeof(object)); 67 | Expression expr = GetExpressionForRule(type, r, paramUser); 68 | 69 | return Expression.Lambda>(expr, paramUser); 70 | } 71 | 72 | public static Func ToFunc(Type type, Rule r) 73 | { 74 | return ToExpression(type, r).Compile(); 75 | } 76 | 77 | public Func CompileRule(Type type, Rule r) 78 | { 79 | var paramUser = Expression.Parameter(typeof(object)); 80 | Expression expr = GetExpressionForRule(type, r, paramUser); 81 | 82 | return Expression.Lambda>(expr, paramUser).Compile(); 83 | } 84 | 85 | public Func CompileRules(IEnumerable rules) 86 | { 87 | var paramUser = Expression.Parameter(typeof(T)); 88 | var expr = BuildNestedExpression(typeof(T), rules, paramUser, ExpressionType.And); 89 | return Expression.Lambda>(expr, paramUser).Compile(); 90 | } 91 | 92 | public Func CompileRules(Type type, IEnumerable rules) 93 | { 94 | var paramUser = Expression.Parameter(type); 95 | var expr = BuildNestedExpression(type, rules, paramUser, ExpressionType.And); 96 | return Expression.Lambda>(expr, paramUser).Compile(); 97 | } 98 | 99 | // Build() in some forks 100 | protected static Expression GetExpressionForRule(Type type, Rule rule, ParameterExpression parameterExpression, bool useTryCatchForNulls = true) 101 | { 102 | ExpressionType nestedOperator; 103 | if (ExpressionType.TryParse(rule.Operator, out nestedOperator) && 104 | _nestedOperators.Contains(nestedOperator) && rule.Rules != null && rule.Rules.Any()) 105 | return BuildNestedExpression(type, rule.Rules, parameterExpression, nestedOperator, useTryCatchForNulls); 106 | else 107 | return BuildExpr(type, rule, parameterExpression, useTryCatchForNulls); 108 | } 109 | 110 | protected static Expression BuildNestedExpression(Type type, IEnumerable rules, ParameterExpression param, 111 | ExpressionType operation, bool useTryCatchForNulls = true) 112 | { 113 | var expressions = rules.Select(r => GetExpressionForRule(type, r, param, useTryCatchForNulls)); 114 | return BinaryExpression(expressions, operation); 115 | } 116 | 117 | protected static Expression BinaryExpression(IEnumerable expressions, ExpressionType operationType) 118 | { 119 | Func methodExp; 120 | switch (operationType) 121 | { 122 | case ExpressionType.Or: 123 | methodExp = Expression.Or; 124 | break; 125 | case ExpressionType.OrElse: 126 | methodExp = Expression.OrElse; 127 | break; 128 | case ExpressionType.AndAlso: 129 | methodExp = Expression.AndAlso; 130 | break; 131 | default: 132 | case ExpressionType.And: 133 | methodExp = Expression.And; 134 | break; 135 | } 136 | 137 | return expressions.Aggregate(methodExp); 138 | } 139 | 140 | private static readonly Regex _regexIndexed = 141 | new Regex(@"(?'Collection'\w+)\[(?:(?'Index'\d+)|(?:['""](?'Key'\w+)[""']))\]", RegexOptions.Compiled); 142 | 143 | private static Expression GetProperty(Expression param, string propname) 144 | { 145 | Expression propExpression = param; 146 | String[] childProperties = propname.Split('.'); 147 | var propertyType = param.Type; 148 | 149 | foreach (var childprop in childProperties) 150 | { 151 | var isIndexed = _regexIndexed.Match(childprop); 152 | if (isIndexed.Success) 153 | { 154 | var indexType = typeof(int); 155 | var collectionname = isIndexed.Groups["Collection"].Value; 156 | var collectionProp = propertyType.GetProperty(collectionname); 157 | if (collectionProp == null) 158 | throw new RulesException( 159 | $"Cannot find collection property {collectionname} in class {propertyType.Name} (\"{propname}\")"); 160 | var collexpr = Expression.PropertyOrField(propExpression, collectionname); 161 | 162 | Expression expIndex; 163 | if (isIndexed.Groups["Index"].Success) 164 | { 165 | var index = Int32.Parse(isIndexed.Groups["Index"].Value); 166 | expIndex = Expression.Constant(index); 167 | } 168 | else 169 | { 170 | expIndex = Expression.Constant(isIndexed.Groups["Key"].Value); 171 | indexType = typeof(string); 172 | } 173 | 174 | var collectionType = collexpr.Type; 175 | if (collectionType.IsArray) 176 | { 177 | propExpression = Expression.ArrayAccess(collexpr, expIndex); 178 | propertyType = propExpression.Type; 179 | } 180 | else 181 | { 182 | var getter = collectionType.GetMethod("get_Item", new Type[] { indexType }); 183 | if (getter == null) 184 | throw new RulesException($"'{collectionname} ({collectionType.Name}) cannot be indexed"); 185 | propExpression = Expression.Call(collexpr, getter, expIndex); 186 | propertyType = getter.ReturnType; 187 | } 188 | } 189 | else 190 | { 191 | var property = propertyType.GetProperty(childprop); 192 | if (property == null) 193 | throw new RulesException( 194 | $"Cannot find property {childprop} in class {propertyType.Name} (\"{propname}\")"); 195 | propExpression = Expression.PropertyOrField(propExpression, childprop); 196 | propertyType = property.PropertyType; 197 | } 198 | } 199 | 200 | return propExpression; 201 | } 202 | 203 | private static Expression BuildEnumerableOperatorExpression(Type type, Rule rule, 204 | ParameterExpression parameterExpression) 205 | { 206 | var collectionPropertyExpression = BuildExpr(type, rule, parameterExpression); 207 | 208 | var itemType = GetCollectionItemType(collectionPropertyExpression.Type); 209 | var expressionParameter = Expression.Parameter(itemType); 210 | 211 | 212 | var genericFunc = typeof(Func<,>).MakeGenericType(itemType, typeof(bool)); 213 | 214 | var innerExp = BuildNestedExpression(itemType, rule.Rules, expressionParameter, ExpressionType.And); 215 | var predicate = Expression.Lambda(genericFunc, innerExp, expressionParameter); 216 | 217 | var body = Expression.Call(typeof(Enumerable), rule.Operator, new[] { itemType }, 218 | collectionPropertyExpression, predicate); 219 | 220 | return body; 221 | } 222 | 223 | private static Type GetCollectionItemType(Type collectionType) 224 | { 225 | if (collectionType.IsArray) 226 | return collectionType.GetElementType(); 227 | 228 | if ((collectionType.GetInterface("IEnumerable") != null)) 229 | return collectionType.GetGenericArguments()[0]; 230 | 231 | return typeof(object); 232 | } 233 | 234 | 235 | private static MethodInfo IsEnumerableOperator(string oprator) 236 | { 237 | return (from tup in _enumrMethodsByName 238 | where string.Equals(oprator, tup.Item1, StringComparison.CurrentCultureIgnoreCase) 239 | select tup.Item2.Value).FirstOrDefault(); 240 | } 241 | 242 | private static Expression BuildExpr(Type type, Rule rule, Expression param, bool useTryCatch = true) 243 | { 244 | Expression propExpression; 245 | Type propType; 246 | 247 | if (param.Type == typeof(object)) 248 | { 249 | param = Expression.TypeAs(param, type); 250 | } 251 | var drule = rule as DataRule; 252 | 253 | if (string.IsNullOrEmpty(rule.MemberName)) //check is against the object itself 254 | { 255 | propExpression = param; 256 | propType = propExpression.Type; 257 | } 258 | else if (drule != null) 259 | { 260 | if (type != typeof(System.Data.DataRow)) 261 | throw new RulesException(" Bad rule"); 262 | propExpression = GetDataRowField(param, drule.MemberName, drule.Type); 263 | propType = propExpression.Type; 264 | } 265 | else 266 | { 267 | propExpression = GetProperty(param, rule.MemberName); 268 | propType = propExpression.Type; 269 | } 270 | if (useTryCatch) 271 | { 272 | propExpression = Expression.TryCatch( 273 | Expression.Block(propExpression.Type, propExpression), 274 | Expression.Catch(typeof(NullReferenceException), Expression.Default(propExpression.Type)) 275 | ); 276 | } 277 | 278 | // is the operator a known .NET operator? 279 | ExpressionType tBinary; 280 | 281 | if (ExpressionType.TryParse(rule.Operator, out tBinary)) 282 | { 283 | Expression right; 284 | var txt = rule.TargetValue as string; 285 | if (txt != null && txt.StartsWith("*.")) 286 | { 287 | txt = txt.Substring(2); 288 | right = GetProperty(param, txt); 289 | } 290 | else 291 | right = StringToExpression(rule.TargetValue, propType); 292 | 293 | return Expression.MakeBinary(tBinary, propExpression, right); 294 | } 295 | 296 | switch (rule.Operator) 297 | { 298 | case "IsMatch": 299 | return Expression.Call( 300 | _miRegexIsMatch.Value, 301 | propExpression, 302 | Expression.Constant(rule.TargetValue, typeof(string)), 303 | Expression.Constant(RegexOptions.IgnoreCase, typeof(RegexOptions)) 304 | ); 305 | case "IsInteger": 306 | return Expression.Call( 307 | _miIntTryParse.Value, 308 | propExpression, 309 | Expression.MakeMemberAccess(null, typeof(Placeholder).GetField("Int")) 310 | ); 311 | case "IsSingle": 312 | return Expression.Call( 313 | _miFloatTryParse.Value, 314 | propExpression, 315 | Expression.MakeMemberAccess(null, typeof(Placeholder).GetField("Float")) 316 | ); 317 | case "IsDouble": 318 | return Expression.Call( 319 | _miDoubleTryParse.Value, 320 | propExpression, 321 | Expression.MakeMemberAccess(null, typeof(Placeholder).GetField("Double")) 322 | ); 323 | case "IsDecimal": 324 | return Expression.Call( 325 | _miDecimalTryParse.Value, 326 | propExpression, 327 | Expression.MakeMemberAccess(null, typeof(Placeholder).GetField("Decimal")) 328 | ); 329 | case "IsInInput": 330 | return Expression.Call(Expression.Constant(rule.Inputs.ToList()), 331 | _miListContains.Value, 332 | propExpression); 333 | default: 334 | break; 335 | } 336 | 337 | var enumrOperation = IsEnumerableOperator(rule.Operator); 338 | if (enumrOperation != null) 339 | { 340 | var elementType = ElementType(propType); 341 | var lambdaParam = Expression.Parameter(elementType, "lambdaParam"); 342 | return rule.Rules?.Any() == true 343 | ? Expression.Call(enumrOperation.MakeGenericMethod(elementType), 344 | propExpression, 345 | Expression.Lambda( 346 | BuildNestedExpression(elementType, rule.Rules, lambdaParam, ExpressionType.AndAlso), 347 | lambdaParam) 348 | 349 | 350 | ) 351 | : Expression.Call(enumrOperation.MakeGenericMethod(elementType), propExpression); 352 | } 353 | else //Invoke a method on the Property 354 | { 355 | var inputs = rule.Inputs.Select(x => x.GetType()).ToArray(); 356 | var methodInfo = propType.GetMethod(rule.Operator, inputs); 357 | List expressions = new List(); 358 | 359 | if (methodInfo == null) 360 | { 361 | methodInfo = propType.GetMethod(rule.Operator); 362 | if (methodInfo != null) 363 | { 364 | var parameters = methodInfo.GetParameters(); 365 | int i = 0; 366 | foreach (var item in rule.Inputs) 367 | { 368 | expressions.Add(MRE.StringToExpression(item, parameters[i].ParameterType)); 369 | i++; 370 | } 371 | } 372 | } 373 | else 374 | expressions.AddRange(rule.Inputs.Select(Expression.Constant)); 375 | if (methodInfo == null) 376 | throw new RulesException($"'{rule.Operator}' is not a method of '{propType.Name}"); 377 | 378 | 379 | if (!methodInfo.IsGenericMethod) 380 | inputs = null; //Only pass in type information to a Generic Method 381 | var callExpression = Expression.Call(propExpression, rule.Operator, inputs, expressions.ToArray()); 382 | if (useTryCatch) 383 | { 384 | return Expression.TryCatch( 385 | Expression.Block(typeof(bool), callExpression), 386 | Expression.Catch(typeof(NullReferenceException), Expression.Constant(false)) 387 | ); 388 | } 389 | else 390 | return callExpression; 391 | } 392 | } 393 | 394 | private static MethodInfo GetLinqMethod(string name, int numParameter) 395 | { 396 | return typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public) 397 | .FirstOrDefault(m => m.Name == name && m.GetParameters().Length == numParameter); 398 | } 399 | 400 | 401 | private static Expression GetDataRowField(Expression prm, string member, string typeName) 402 | { 403 | var expMember = Expression.Call(prm, _miGetItem.Value, Expression.Constant(member, typeof(string))); 404 | var type = Type.GetType(typeName); 405 | Debug.Assert(type != null); 406 | 407 | if (type.IsClass || typeName.StartsWith("System.Nullable")) 408 | { 409 | // equals "return testValue == DBNull.Value ? (typeName) null : (typeName) testValue" 410 | return Expression.Condition(Expression.Equal(expMember, Expression.Constant(DBNull.Value)), 411 | Expression.Constant(null, type), 412 | Expression.Convert(expMember, type)); 413 | } 414 | else 415 | // equals "return (typeName) testValue" 416 | return Expression.Convert(expMember, type); 417 | } 418 | 419 | private static Expression StringToExpression(object value, Type propType) 420 | { 421 | Debug.Assert(propType != null); 422 | 423 | object safevalue; 424 | Type valuetype = propType; 425 | var txt = value as string; 426 | if (value == null) 427 | { 428 | safevalue = null; 429 | } 430 | else if (txt != null) 431 | { 432 | if (txt.ToLower() == "null") 433 | safevalue = null; 434 | else if (propType.IsEnum) 435 | safevalue = Enum.Parse(propType, txt); 436 | else 437 | { 438 | if (propType.Name == "Nullable`1") 439 | valuetype = Nullable.GetUnderlyingType(propType); 440 | 441 | safevalue = IsTime(txt, propType) ?? Convert.ChangeType(value, valuetype); 442 | } 443 | } 444 | else 445 | { 446 | if (propType.Name == "Nullable`1") 447 | valuetype = Nullable.GetUnderlyingType(propType); 448 | safevalue = Convert.ChangeType(value, valuetype); 449 | } 450 | 451 | return Expression.Constant(safevalue, propType); 452 | } 453 | 454 | private static readonly Regex reNow = new Regex(@"#NOW([-+])(\d+)([SMHDY])", RegexOptions.IgnoreCase 455 | | RegexOptions.Compiled 456 | | RegexOptions.Singleline); 457 | 458 | private static DateTime? IsTime(string text, Type targetType) 459 | { 460 | if (targetType != typeof(DateTime) && targetType != typeof(DateTime?)) 461 | return null; 462 | 463 | var match = reNow.Match(text); 464 | if (!match.Success) 465 | return null; 466 | 467 | var amt = Int32.Parse(match.Groups[2].Value); 468 | if (match.Groups[1].Value == "-") 469 | amt = -amt; 470 | 471 | switch (Char.ToUpperInvariant(match.Groups[3].Value[0])) 472 | { 473 | case 'S': 474 | return DateTime.Now.AddSeconds(amt); 475 | case 'M': 476 | return DateTime.Now.AddMinutes(amt); 477 | case 'H': 478 | return DateTime.Now.AddHours(amt); 479 | case 'D': 480 | return DateTime.Now.AddDays(amt); 481 | case 'Y': 482 | return DateTime.Now.AddYears(amt); 483 | } 484 | // it should not be possible to reach here. 485 | throw new ArgumentException(); 486 | } 487 | 488 | private static Type ElementType(Type seqType) 489 | { 490 | Type ienum = FindIEnumerable(seqType); 491 | if (ienum == null) return seqType; 492 | return ienum.GetGenericArguments()[0]; 493 | } 494 | 495 | private static Type FindIEnumerable(Type seqType) 496 | { 497 | if (seqType == null || seqType == typeof(string)) 498 | return null; 499 | if (seqType.IsArray) 500 | return typeof(IEnumerable<>).MakeGenericType(seqType.GetElementType()); 501 | if (seqType.IsGenericType) 502 | { 503 | foreach (Type arg in seqType.GetGenericArguments()) 504 | { 505 | Type ienum = typeof(IEnumerable<>).MakeGenericType(arg); 506 | if (ienum.IsAssignableFrom(seqType)) 507 | { 508 | return ienum; 509 | } 510 | } 511 | } 512 | 513 | Type[] ifaces = seqType.GetInterfaces(); 514 | foreach (Type iface in ifaces) 515 | { 516 | Type ienum = FindIEnumerable(iface); 517 | if (ienum != null) 518 | return ienum; 519 | } 520 | 521 | if (seqType.BaseType != null && seqType.BaseType != typeof(object)) 522 | { 523 | return FindIEnumerable(seqType.BaseType); 524 | } 525 | 526 | return null; 527 | } 528 | public enum OperatorType 529 | { 530 | InternalString = 1, 531 | ObjectMethod = 2, 532 | Comparison = 3, 533 | Logic = 4 534 | } 535 | public class Operator 536 | { 537 | public string Name { get; set; } 538 | public OperatorType Type { get; set; } 539 | public int NumberOfInputs { get; set; } 540 | public bool SimpleInputs { get; set; } 541 | } 542 | public class Member 543 | { 544 | public string Name { get; set; } 545 | public string Type { get; set; } 546 | public List PossibleOperators { get; set; } 547 | public static bool IsSimpleType(Type type) 548 | { 549 | return 550 | type.IsPrimitive || 551 | new Type[] { 552 | typeof(Enum), 553 | typeof(String), 554 | typeof(Decimal), 555 | typeof(DateTime), 556 | typeof(DateTimeOffset), 557 | typeof(TimeSpan), 558 | typeof(Guid) 559 | }.Contains(type) || 560 | Convert.GetTypeCode(type) != TypeCode.Object || 561 | (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>) && IsSimpleType(type.GetGenericArguments()[0])) 562 | ; 563 | } 564 | public static BindingFlags flags = BindingFlags.Instance | BindingFlags.Public; 565 | public static List GetFields(Type type, string memberName = null, string parentPath = null) 566 | { 567 | List toReturn = new List(); 568 | var fi = new Member 569 | { 570 | Name = string.IsNullOrEmpty(parentPath) ? memberName : $"{parentPath}.{memberName}", 571 | Type = type.ToString() 572 | }; 573 | fi.PossibleOperators = Member.Operators(type, string.IsNullOrEmpty(fi.Name)); 574 | toReturn.Add(fi); 575 | if (!Member.IsSimpleType(type)) 576 | { 577 | var fields = type.GetFields(Member.flags); 578 | var properties = type.GetProperties(Member.flags); 579 | foreach (var field in fields) 580 | { 581 | string useParentName = null; 582 | var name = Member.ValidateName(field.Name, type, memberName, fi.Name, parentPath, out useParentName); 583 | toReturn.AddRange(GetFields(field.FieldType, name, useParentName)); 584 | } 585 | foreach (var prop in properties) 586 | { 587 | string useParentName = null; 588 | var name = Member.ValidateName(prop.Name, type, memberName, fi.Name, parentPath, out useParentName); 589 | toReturn.AddRange(GetFields(prop.PropertyType, name, useParentName)); 590 | } 591 | } 592 | return toReturn; 593 | } 594 | private static string ValidateName(string name, Type parentType, string parentName, string parentPath, string grandparentPath, out string useAsParentPath) 595 | { 596 | if (name == "Item" && IsGenericList(parentType)) 597 | { 598 | useAsParentPath = grandparentPath; 599 | return parentName + "[0]"; 600 | } 601 | else 602 | { 603 | useAsParentPath = parentPath; 604 | return name; 605 | } 606 | } 607 | public static bool IsGenericList(Type type) 608 | { 609 | if (type == null) 610 | { 611 | throw new ArgumentNullException("type"); 612 | } 613 | foreach (Type @interface in type.GetInterfaces()) 614 | { 615 | if (@interface.IsGenericType) 616 | { 617 | if (@interface.GetGenericTypeDefinition() == typeof(ICollection<>)) 618 | { 619 | // if needed, you can also return the type used as generic argument 620 | return true; 621 | } 622 | } 623 | } 624 | return false; 625 | } 626 | private static string[] logicOperators = new string[] { 627 | mreOperator.And.ToString("g"), 628 | mreOperator.AndAlso.ToString("g"), 629 | mreOperator.Or.ToString("g"), 630 | mreOperator.OrElse.ToString("g") 631 | }; 632 | private static string[] comparisonOperators = new string[] { 633 | mreOperator.Equal.ToString("g"), 634 | mreOperator.GreaterThan.ToString("g"), 635 | mreOperator.GreaterThanOrEqual.ToString("g"), 636 | mreOperator.LessThan.ToString("g"), 637 | mreOperator.LessThanOrEqual.ToString("g"), 638 | mreOperator.NotEqual.ToString("g"), 639 | }; 640 | 641 | private static string[] hardCodedStringOperators = new string[] { 642 | mreOperator.IsMatch.ToString("g"), 643 | mreOperator.IsInteger.ToString("g"), 644 | mreOperator.IsSingle.ToString("g"), 645 | mreOperator.IsDouble.ToString("g"), 646 | mreOperator.IsDecimal.ToString("g"), 647 | mreOperator.IsInInput.ToString("g"), 648 | }; 649 | public static List Operators(Type type, bool addLogicOperators = false, bool noOverloads = true) 650 | { 651 | List operators = new List(); 652 | if (addLogicOperators) 653 | { 654 | operators.AddRange(logicOperators.Select(x => new Operator() { Name = x, Type = OperatorType.Logic })); 655 | } 656 | 657 | if (type == typeof(String)) 658 | { 659 | operators.AddRange(hardCodedStringOperators.Select(x => new Operator() { Name = x, Type = OperatorType.InternalString })); 660 | } 661 | else if (Member.IsSimpleType(type)) 662 | { 663 | operators.AddRange(comparisonOperators.Select(x => new Operator() { Name = x, Type = OperatorType.Comparison })); 664 | } 665 | var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance); 666 | foreach (var method in methods) 667 | { 668 | if (method.ReturnType == typeof(Boolean) && !method.Name.StartsWith("get_") && !method.Name.StartsWith("set_") && !method.Name.StartsWith("_op")) 669 | { 670 | var paramaters = method.GetParameters(); 671 | var op = new Operator() 672 | { 673 | Name = method.Name, 674 | Type = OperatorType.ObjectMethod, 675 | NumberOfInputs = paramaters.Length, 676 | SimpleInputs = paramaters.All(x => Member.IsSimpleType(x.ParameterType)) 677 | }; 678 | if (noOverloads) 679 | { 680 | var existing = operators.FirstOrDefault(x => x.Name == op.Name && x.Type == op.Type); 681 | if (existing == null) 682 | operators.Add(op); 683 | else if (existing.NumberOfInputs > op.NumberOfInputs) 684 | { 685 | operators[operators.IndexOf(existing)] = op; 686 | } 687 | } 688 | else 689 | operators.Add(op); 690 | } 691 | } 692 | return operators; 693 | } 694 | } 695 | 696 | } 697 | 698 | [DataContract] 699 | public class Rule 700 | { 701 | public Rule() 702 | { 703 | Inputs = Enumerable.Empty(); 704 | } 705 | 706 | [DataMember] public string MemberName { get; set; } 707 | [DataMember] public string Operator { get; set; } 708 | [DataMember] public object TargetValue { get; set; } 709 | [DataMember] public IList Rules { get; set; } 710 | [DataMember] public IEnumerable Inputs { get; set; } 711 | 712 | 713 | public static Rule operator |(Rule lhs, Rule rhs) 714 | { 715 | var rule = new Rule { Operator = "Or" }; 716 | return MergeRulesInto(rule, lhs, rhs); 717 | } 718 | 719 | public static Rule operator &(Rule lhs, Rule rhs) 720 | { 721 | var rule = new Rule { Operator = "AndAlso" }; 722 | return MergeRulesInto(rule, lhs, rhs); 723 | } 724 | 725 | private static Rule MergeRulesInto(Rule target, Rule lhs, Rule rhs) 726 | { 727 | target.Rules = new List(); 728 | 729 | if (lhs.Rules != null && lhs.Operator == target.Operator) // left is multiple 730 | { 731 | target.Rules.AddRange(lhs.Rules); 732 | if (rhs.Rules != null && rhs.Operator == target.Operator) 733 | target.Rules.AddRange(rhs.Rules); // left & right are multiple 734 | else 735 | target.Rules.Add(rhs); // left multi, right single 736 | } 737 | else if (rhs.Rules != null && rhs.Operator == target.Operator) 738 | { 739 | target.Rules.Add(lhs); // left single, right multi 740 | target.Rules.AddRange(rhs.Rules); 741 | } 742 | else 743 | { 744 | target.Rules.Add(lhs); 745 | target.Rules.Add(rhs); 746 | } 747 | 748 | 749 | return target; 750 | } 751 | 752 | public static Rule Create(string member, mreOperator oper, object target) 753 | { 754 | return new Rule { MemberName = member, TargetValue = target, Operator = oper.ToString() }; 755 | } 756 | 757 | public static Rule MethodOnChild(string member, string methodName, params object[] inputs) 758 | { 759 | return new Rule { MemberName = member, Inputs = inputs.ToList(), Operator = methodName }; 760 | } 761 | 762 | public static Rule Method(string methodName, params object[] inputs) 763 | { 764 | return new Rule { Inputs = inputs.ToList(), Operator = methodName }; 765 | } 766 | 767 | public static Rule Any(string member, Rule rule) 768 | { 769 | return new Rule { MemberName = member, Operator = "Any", Rules = new List { rule } }; 770 | } 771 | 772 | public static Rule All(string member, Rule rule) 773 | { 774 | return new Rule { MemberName = member, Operator = "All", Rules = new List { rule } }; 775 | } 776 | 777 | public static Rule IsInteger(string member) => new Rule() { MemberName = member, Operator = "IsInteger" }; 778 | public static Rule IsFloat(string member) => new Rule() { MemberName = member, Operator = "IsSingle" }; 779 | public static Rule IsDouble(string member) => new Rule() { MemberName = member, Operator = "IsDouble" }; 780 | public static Rule IsSingle(string member) => new Rule() { MemberName = member, Operator = "IsSingle" }; 781 | public static Rule IsDecimal(string member) => new Rule() { MemberName = member, Operator = "IsDecimal" }; 782 | 783 | 784 | 785 | public override string ToString() 786 | { 787 | if (TargetValue != null) 788 | return $"{MemberName} {Operator} {TargetValue}"; 789 | 790 | if (Rules != null) 791 | { 792 | if (Rules.Count == 1) 793 | return $"{MemberName} {Operator} ({Rules[0]})"; 794 | else 795 | return $"{MemberName} {Operator} (Rules)"; 796 | } 797 | 798 | if (Inputs != null) 799 | { 800 | return $"{MemberName} {Operator} (Inputs)"; 801 | } 802 | 803 | return $"{MemberName} {Operator}"; 804 | } 805 | } 806 | 807 | public class DataRule : Rule 808 | { 809 | public string Type { get; set; } 810 | 811 | public static DataRule Create(string member, mreOperator oper, T target) 812 | { 813 | return new DataRule 814 | { 815 | MemberName = member, 816 | TargetValue = target, 817 | Operator = oper.ToString(), 818 | Type = typeof(T).FullName 819 | }; 820 | } 821 | 822 | public static DataRule Create(string member, mreOperator oper, string target) 823 | { 824 | return new DataRule 825 | { 826 | MemberName = member, 827 | TargetValue = target, 828 | Operator = oper.ToString(), 829 | Type = typeof(T).FullName 830 | }; 831 | } 832 | 833 | 834 | public static DataRule Create(string member, mreOperator oper, object target, Type memberType) 835 | { 836 | return new DataRule 837 | { 838 | MemberName = member, 839 | TargetValue = target, 840 | Operator = oper.ToString(), 841 | Type = memberType.FullName 842 | }; 843 | } 844 | } 845 | 846 | internal static class Placeholder 847 | { 848 | public static int Int = 0; 849 | public static float Float=0.0f; 850 | public static double Double=0.0; 851 | public static decimal Decimal=0.0m; 852 | } 853 | 854 | // Nothing specific to MRE. Can be moved to a more common location 855 | public static class Extensions 856 | { 857 | public static void AddRange(this IList collection, IEnumerable newValues) 858 | { 859 | foreach (var item in newValues) 860 | collection.Add(item); 861 | } 862 | } 863 | 864 | public class RulesException : ApplicationException 865 | { 866 | public RulesException() 867 | { 868 | } 869 | 870 | public RulesException(string message) : base(message) 871 | { 872 | } 873 | 874 | public RulesException(string message, Exception innerException) : base(message, innerException) 875 | { 876 | } 877 | } 878 | 879 | 880 | 881 | // 882 | // Summary: 883 | // Describes the node types for the nodes of an expression tree. 884 | public enum mreOperator 885 | { 886 | // 887 | // Summary: 888 | // An addition operation, such as a + b, without overflow checking, for numeric 889 | // operands. 890 | Add = 0, 891 | // 892 | // Summary: 893 | // A bitwise or logical AND operation, such as (a & b) in C# and (a And b) in Visual 894 | // Basic. 895 | And = 2, 896 | // 897 | // Summary: 898 | // A conditional AND operation that evaluates the second operand only if the first 899 | // operand evaluates to true. It corresponds to (a && b) in C# and (a AndAlso b) 900 | // in Visual Basic. 901 | AndAlso = 3, 902 | // 903 | // Summary: 904 | // A node that represents an equality comparison, such as (a == b) in C# or (a = 905 | // b) in Visual Basic. 906 | Equal = 13, 907 | // 908 | // Summary: 909 | // A "greater than" comparison, such as (a > b). 910 | GreaterThan = 15, 911 | // 912 | // Summary: 913 | // A "greater than or equal to" comparison, such as (a >= b). 914 | GreaterThanOrEqual = 16, 915 | // 916 | // Summary: 917 | // A "less than" comparison, such as (a < b). 918 | LessThan = 20, 919 | // 920 | // Summary: 921 | // A "less than or equal to" comparison, such as (a <= b). 922 | LessThanOrEqual = 21, 923 | // 924 | // Summary: 925 | // An inequality comparison, such as (a != b) in C# or (a <> b) in Visual Basic. 926 | NotEqual = 35, 927 | // 928 | // Summary: 929 | // A bitwise or logical OR operation, such as (a | b) in C# or (a Or b) in Visual 930 | // Basic. 931 | Or = 36, 932 | // 933 | // Summary: 934 | // A short-circuiting conditional OR operation, such as (a || b) in C# or (a OrElse 935 | // b) in Visual Basic. 936 | OrElse = 37, 937 | /// 938 | /// Checks that a string value matches a Regex expression 939 | /// 940 | IsMatch = 100, 941 | /// 942 | /// Checks that a value can be 'TryParsed' to an Int32 943 | /// 944 | IsInteger = 101, 945 | /// 946 | /// Checks that a value can be 'TryParsed' to a Single 947 | /// 948 | IsSingle = 102, 949 | /// 950 | /// Checks that a value can be 'TryParsed' to a Double 951 | /// 952 | IsDouble = 103, 953 | /// 954 | /// Checks that a value can be 'TryParsed' to a Decimal 955 | /// 956 | IsDecimal = 104, 957 | /// 958 | /// Checks if the value of the property is in the input list 959 | /// 960 | IsInInput = 105 961 | } 962 | 963 | 964 | public class RuleValue 965 | { 966 | public T Value { get; set; } 967 | public List Rules { get; set; } 968 | } 969 | 970 | public class RuleValueString : RuleValue 971 | { 972 | } 973 | } 974 | -------------------------------------------------------------------------------- /MicroRuleEngine/MicroRuleEngine.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net40;net452;net461;netstandard2.0; netstandard2.1;netcoreapp2.1;netcoreapp3.1 5 | PackageReference 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Nuget/CreatePackage.bat: -------------------------------------------------------------------------------- 1 | mkdir content 2 | mkdir content\MRE 3 | 4 | copy ..\MicroRuleEngine\MRE.cs content\MRE 5 | nuget pack MRE.nuspec -Exclude *.bat 6 | nuget push MRE.1.0.2.nupkg -Source https://api.nuget.org/v3/index.json 7 | 8 | pause -------------------------------------------------------------------------------- /Nuget/MRE.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MRE 5 | 1.0.2 6 | runxc1,jamescurran 7 | runxc1 8 | https://github.com/runxc1/MicroRuleEngine 9 | false 10 | The Micro Rule Engine is a small(As in about 200 lines) Rule Engine that allows you to create Business rules that are not hard coded. Under the hood MRE creates a Linq Expression tree and compies a rule into an anonymous method i.e. Func<T bool> 11 | Long awaited release adding a new api. 12 | Copyright 2019 13 | dotnet RuleEngine Micro Rule-Engine Expression-Tree Rule Engine 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MicroRuleEngine is a single file rule engine 2 | ============================================ 3 | 4 | A `.Net` Rule Engine for **dynamically** evaluating business rules compiled on the fly. If you have business rules that you don't want to hard code then the `MicroRuleEngine` is your friend. The rule engine is easy to groc and is only about 200 lines of code. Under the covers it creates a `Linq` expression tree that is compiled so even if your business rules get pretty large or you run them against thousands of items the performance should still compare nicely with a hard coded solution. 5 | 6 | How To Install It? 7 | ------------------ 8 | Drop the code file into your app and change it as you wish. 9 | 10 | How Do You Use It? 11 | ------------------ 12 | The best examples of how to use the `MicroRuleEngine (MRE)` can be found in the Test project included in the Solution. 13 | 14 | One of the tests: 15 | ```csharp 16 | [TestMethod] 17 | public void ChildProperties() 18 | { 19 | Order order = this.GetOrder(); 20 | Rule rule = new Rule() 21 | { 22 | MemberName = "Customer.Country.CountryCode", 23 | Operator = System.Linq.Expressions.ExpressionType.Equal.ToString("g"), 24 | TargetValue = "AUS" 25 | }; 26 | MRE engine = new MRE(); 27 | var compiledRule = engine.CompileRule(rule); 28 | bool passes = compiledRule(order); 29 | Assert.IsTrue(passes); 30 | 31 | order.Customer.Country.CountryCode = "USA"; 32 | passes = compiledRule(order); 33 | Assert.IsFalse(passes); 34 | } 35 | ``` 36 | 37 | What Kinds of Rules can I express 38 | -------------------------------- 39 | In addition to comparative operators such as `Equals`, `GreaterThan`, `LessThan` etc. You can also call methods on the object that return a `boolean` value such as `Contains` or `StartsWith` on a string. In addition to comparative operators, additional operators such as `IsMatch` or `IsInteger` have been added and demonstrates how you could edit the code to add your own operator(s). Rules can also be `AND`'d or `OR`'d together: 40 | ```csharp 41 | 42 | Rule rule = 43 | Rule.Create("Customer.LastName", "Contains", "Do") 44 | & ( 45 | Rule.Create("Customer.FirstName", "StartsWith", "Jo") 46 | | Rule.Create("Customer.FirstName", "StartsWith", "Bob") 47 | ); 48 | ``` 49 | 50 | You can reference member properties which are `Arrays` or `List<>` by their index: 51 | ```csharp 52 | Rule rule = Rule.Create("Items[1].Cost", mreOperator.GreaterThanOrEqual, "5.25"); 53 | ``` 54 | 55 | Similarly, you can reference element of a string- or integer-keyed dictionary: 56 | ```csharp 57 | Rule rule = Rule.Create("Items['myKey'].Cost", mreOperator.GreaterThanOrEqual, "5.25"); 58 | ``` 59 | 60 | 61 | You can also compare an object to itself indicated by the `*.` at the beginning of the `TargetValue`: 62 | ```csharp 63 | Rule rule = Rule.Create("Items[1].Cost", mreOperator.Equal, "*.Items[0].Cost"); 64 | ``` 65 | 66 | There are a lot of examples in the test cases but, here is another snippet demonstrating nested `OR` logic: 67 | ```csharp 68 | [TestMethod] 69 | public void ConditionalLogic() 70 | { 71 | Order order = this.GetOrder(); 72 | Rule rule = new Rule() 73 | { 74 | Operator = "AndAlso", 75 | Rules = new List() 76 | { 77 | new Rule() { MemberName = "Customer.LastName", TargetValue = "Doe", Operator = "Equal"}, 78 | new Rule() { 79 | Operator = "Or", 80 | Rules = new List() { 81 | new Rule(){ MemberName = "Customer.FirstName", TargetValue = "John", Operator = "Equal"}, 82 | new Rule(){ MemberName = "Customer.FirstName", TargetValue = "Judy", Operator = "Equal"} 83 | } 84 | } 85 | } 86 | }; 87 | MRE engine = new MRE(); 88 | var fakeName = engine.CompileRule(rule); 89 | bool passes = fakeName(order); 90 | Assert.IsTrue(passes); 91 | 92 | order.Customer.FirstName = "Philip"; 93 | passes = fakeName(order); 94 | Assert.IsFalse(passes); 95 | } 96 | 97 | ``` 98 | 99 | If you need to run your comparison against an ADO.NET DataSet you can also do that as well: 100 | ```csharp 101 | var dr = GetDataRow(); 102 | // (int) dr["Column2"] == 123 && (string) dr["Column1"] == "Test" 103 | Rule rule = DataRule.Create("Column2", mreOperator.Equal, "123") & DataRule.Create("Column1", mreOperator.Equal, "Test"); 104 | ``` 105 | 106 | 107 | 108 | #### #NOW and time-based rules. 109 | You can test a property for a time range from the current time, using the special case `#NOW` keyword. The member must be a `DataTime` or `DateTime?`, 110 | and the target value must be a string in the form :`#NOW+90D` (The sign can be plus or minus, but must be given. The Suffix can be 111 | 'S' for Seconds, `M` for Minutes, `H` for Hours, `D` for Days, or `Y` for Years. The number must be an integer.) 112 | 113 | examples: 114 | 115 | ` Rule rule = Rule.Create("OrderDate", mreOperator.GreaterThanOrEqual, "#NOW-90M");` 116 | 117 | `OrderDate` must be within the last 90 minutes. 118 | 119 | ` Rule rule = Rule.Create("ExpirationDate", mreOperator.LessThanOrEqual, "#NOW+1Y");` 120 | 121 | `ExpirationDate` must be within the next year. 122 | 123 | 124 | 125 | 126 | How Can I Store Rules? 127 | --------------------- 128 | The `Rule` Class is just a **POCO** so you can store your rules as serialized `XML`, `JSON`, etc. 129 | 130 | #### Forked many times and now updated to pull in a lot of the great work done by jamescurran, nazimkov and others that help improve the API 131 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 runxc1(Bret Ferrier) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | --------------------------------------------------------------------------------