diff --git a/.gitignore b/.gitignore index fda73e7..621ed09 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ content /ConsoleApp1-2.psess /ConsoleApp1-1.psess /MicroRuleEngine/MRE1.cs +privateKey.key diff --git a/MicroRuleEngine.Core.Tests/ExampleUsage.cs b/MicroRuleEngine.Core.Tests/ExampleUsage.cs new file mode 100644 index 0000000..42482a6 --- /dev/null +++ b/MicroRuleEngine.Core.Tests/ExampleUsage.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MicroRuleEngine.Core.Tests.Models; + +namespace MicroRuleEngine.Tests +{ + /// + /// Summary description for UnitTest1 + /// + [TestClass] + public class ExampleUsage + { + [TestMethod] + public void ChildPropertiesOfNull() + { + Order order = GetOrder(); + order.Customer = null; + Rule rule = new Rule + { + MemberName = "Customer.Country.CountryCode", + Operator = ExpressionType.Equal.ToString("g"), + TargetValue = "AUS" + }; + MRE engine = new MRE(); + var compiledRule = engine.CompileRule(rule); + bool passes = compiledRule(order); + Assert.IsFalse(passes); + } + + [TestMethod] + public void GetAllField() + { + Order order = GetOrder(); + + var type = order.GetType(); + var members = MRE.Member.GetFields(type); + Assert.IsTrue( + members.Where(x=> x.Name == "Customer.Country.CountryCode" && x.PossibleOperators.Any(y=> y.Name == "StartsWith")).Any() + ); + } + + [TestMethod] + public void CoerceMethod() + { + Order order = GetOrder(); + Rule rule = new Rule + { + MemberName = "Codes", + Operator = "Contains", + TargetValue = "243", + Inputs = new List() { "243" } + }; + MRE engine = new MRE(); + var compiledRule = engine.CompileRule(rule); + bool passes = compiledRule(order); + Assert.IsTrue(passes); + + order.Codes.Clear(); + passes = compiledRule(order); + Assert.IsFalse(passes); + } + + [TestMethod] + public void ChildProperties() + { + Order order = GetOrder(); + Rule rule = new Rule + { + MemberName = "Customer.Country.CountryCode", + Operator = ExpressionType.Equal.ToString("g"), + TargetValue = "AUS" + }; + MRE engine = new MRE(); + var compiledRule = engine.CompileRule(rule); + bool passes = compiledRule(order); + Assert.IsTrue(passes); + + order.Customer.Country.CountryCode = "USA"; + passes = compiledRule(order); + Assert.IsFalse(passes); + } + + [TestMethod] + + public void ConditionalLogic() + { + Order order = GetOrder(); + Rule rule = new Rule + { + Operator = ExpressionType.AndAlso.ToString("g"), + Rules = new List + { + new Rule { MemberName = "Customer.LastName", TargetValue = "Doe", Operator = "Equal"}, + new Rule + { + Operator = "Or", + Rules = new List + { + new Rule { MemberName = "Customer.FirstName", TargetValue = "John", Operator = "Equal"}, + new Rule { MemberName = "Customer.FirstName", TargetValue = "Jane", Operator = "Equal"} + } + } + } + }; + MRE engine = new MRE(); + var fakeName = engine.CompileRule(rule); + bool passes = fakeName(order); + Assert.IsTrue(passes); + + order.Customer.FirstName = "Philip"; + passes = fakeName(order); + Assert.IsFalse(passes); + } + + [TestMethod] + public void BooleanMethods() + { + Order order = GetOrder(); + Rule rule = new Rule + { + Operator = "HasItem",//The Order Object Contains a method named 'HasItem' that returns true/false + Inputs = new List { "Test" } + }; + MRE engine = new MRE(); + + var boolMethod = engine.CompileRule(rule); + bool passes = boolMethod(order); + Assert.IsTrue(passes); + + var item = order.Items.First(x => x.ItemCode == "Test"); + item.ItemCode = "Changed"; + passes = boolMethod(order); + Assert.IsFalse(passes); + } + + [TestMethod] + public void BooleanMethods_ByType() + { + Order order = GetOrder(); + Rule rule = new Rule + { + Operator = "HasItem",//The Order Object Contains a method named 'HasItem' that returns true/false + Inputs = new List { "Test" } + }; + MRE engine = new MRE(); + + var boolMethod = engine.CompileRule(typeof(Order), rule); + bool passes =(bool) boolMethod.DynamicInvoke(order); + Assert.IsTrue(passes); + + var item = order.Items.First(x => x.ItemCode == "Test"); + item.ItemCode = "Changed"; + passes = (bool)boolMethod.DynamicInvoke(order); + Assert.IsFalse(passes); + } + + [TestMethod] + public void AnyOperator() + { + Order order = GetOrder(); + //order.Items.Any(a => a.ItemCode == "test") + Rule rule = new Rule + { + MemberName = "Items",// The array property + Operator = "Any", + Rules = new[] + { + new Rule + { + MemberName = "ItemCode", // the property in the above array item + Operator = "Equal", + TargetValue = "Test", + } + } + }; + MRE engine = new MRE(); + var boolMethod = engine.CompileRule(rule); + bool passes = boolMethod(order); + Assert.IsTrue(passes); + + var item = order.Items.First(x => x.ItemCode == "Test"); + item.ItemCode = "Changed"; + passes = boolMethod(order); + Assert.IsFalse(passes); + } + + + + [TestMethod] + public void ChildPropertyBooleanMethods() + { + Order order = GetOrder(); + Rule rule = new Rule + { + MemberName = "Customer.FirstName", + Operator = "EndsWith",//Regular method that exists on string.. As a note expression methods are not available + Inputs = new List { "ohn" } + }; + MRE engine = new MRE(); + var childPropCheck = engine.CompileRule(rule); + bool passes = childPropCheck(order); + Assert.IsTrue(passes); + + order.Customer.FirstName = "jane"; + passes = childPropCheck(order); + Assert.IsFalse(passes); + } + + [TestMethod] + public void ChildPropertyOfNullBooleanMethods() + { + Order order = GetOrder(); + order.Customer = null; + Rule rule = new Rule + { + MemberName = "Customer.FirstName", + Operator = "EndsWith", //Regular method that exists on string.. As a note expression methods are not available + Inputs = new List { "ohn" } + }; + MRE engine = new MRE(); + var childPropCheck = engine.CompileRule(rule); + bool passes = childPropCheck(order); + Assert.IsFalse(passes); + } + [TestMethod] + public void RegexIsMatch()//Had to add a Regex evaluator to make it feel 'Complete' + { + Order order = GetOrder(); + Rule rule = new Rule + { + MemberName = "Customer.FirstName", + Operator = "IsMatch", + TargetValue = @"^[a-zA-Z0-9]*$" + }; + MRE engine = new MRE(); + var regexCheck = engine.CompileRule(rule); + bool passes = regexCheck(order); + Assert.IsTrue(passes); + + order.Customer.FirstName = "--NoName"; + passes = regexCheck(order); + Assert.IsFalse(passes); + } + + public static Order GetOrder() + { + Order order = new Order() + { + OrderId = 1, + Customer = new Customer() + { + FirstName = "John", + LastName = "Doe", + Country = new Country() + { + CountryCode = "AUS" + } + }, + Items = new List() + { + new Item { ItemCode = "MM23", Cost=5.25M}, + new Item { ItemCode = "LD45", Cost=5.25M}, + new Item { ItemCode = "Test", Cost=3.33M}, + }, + Codes = new List() + { + 555, + 321, + 243 + }, + Total = 13.83m, + OrderDate = new DateTime(1776, 7, 4), + Status = Status.Open + + }; + return order; + } + } +} diff --git a/MicroRuleEngine.Core.Tests/ExpressionToSQLQueryTest.cs b/MicroRuleEngine.Core.Tests/ExpressionToSQLQueryTest.cs new file mode 100644 index 0000000..5df35d9 --- /dev/null +++ b/MicroRuleEngine.Core.Tests/ExpressionToSQLQueryTest.cs @@ -0,0 +1,57 @@ +using EFModeling.Samples.DataSeeding; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MicroRuleEngine.Core.Tests +{ + [TestClass] + public class ExpressionToSQLQueryTest + { + internal DbContextOptions GetDBOptions() + { + var connection = new SqliteConnection("DataSource=:memory:"); + connection.Open(); + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + + // Create the schema in the database + using (var context = new BloggingContext(options)) + { + context.Database.EnsureCreated(); + } + return options; + } + [TestMethod] + public void BasicEqualityExpression() + { + + using (var context = new BloggingContext(GetDBOptions())) + { + context.Blogs.Add(new Blog { Url = "http://test.com" }); + context.SaveChanges(); + + var testBlog = context.Blogs.FirstOrDefault(b => b.Url == "http://test.com"); + + var fields = MRE.Member.GetFields(typeof(Blog)); + Rule rule = new Rule + { + MemberName = "Url", + Operator = mreOperator.Equal.ToString("g"), + TargetValue = "http://test.com" + }; + + var blog2 = context.Blogs.Where(MRE.ToExpression(rule, false)).FirstOrDefault(); + + Assert.IsTrue(testBlog.BlogId == blog2.BlogId); + + } + } + } +} diff --git a/MicroRuleEngine.Core.Tests/MicroRuleEngine.Core.Tests.csproj b/MicroRuleEngine.Core.Tests/MicroRuleEngine.Core.Tests.csproj new file mode 100644 index 0000000..30b5bcc --- /dev/null +++ b/MicroRuleEngine.Core.Tests/MicroRuleEngine.Core.Tests.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp2.1 + + false + + + + + + + + + + + + + + diff --git a/MicroRuleEngine.Core.Tests/Models/Blog.cs b/MicroRuleEngine.Core.Tests/Models/Blog.cs new file mode 100644 index 0000000..5213295 --- /dev/null +++ b/MicroRuleEngine.Core.Tests/Models/Blog.cs @@ -0,0 +1,72 @@ +using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; + +namespace EFModeling.Samples.DataSeeding +{ + public class Blog + { + public int BlogId { get; set; } + public string Url { get; set; } + public virtual ICollection Posts { get; set; } + } + public class Post + { + public int PostId { get; set; } + public string Content { get; set; } + public string Title { get; set; } + public int BlogId { get; set; } + public Blog Blog { get; set; } + public Name AuthorName { get; set; } + } + public class Name + { + public virtual string First { get; set; } + public virtual string Last { get; set; } + } + public class BloggingContext : DbContext + { + public DbSet Blogs { get; set; } + public DbSet Posts { get; set; } + + public BloggingContext(DbContextOptions options) + : base(options) + { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.Property(e => e.Url).IsRequired(); + }); + + #region BlogSeed + modelBuilder.Entity().HasData(new Blog { BlogId = 1, Url = "http://sample.com" }); + #endregion + + modelBuilder.Entity(entity => + { + entity.HasOne(d => d.Blog) + .WithMany(p => p.Posts) + .HasForeignKey("BlogId"); + }); + + #region PostSeed + modelBuilder.Entity().HasData( + new Post() { BlogId = 1, PostId = 1, Title = "First post", Content = "Test 1" }); + #endregion + + #region AnonymousPostSeed + modelBuilder.Entity().HasData( + new { BlogId = 1, PostId = 2, Title = "Second post", Content = "Test 2" }); + #endregion + + #region OwnedTypeSeed + modelBuilder.Entity().OwnsOne(p => p.AuthorName).HasData( + new { PostId = 1, First = "Andriy", Last = "Svyryd" }, + new { PostId = 2, First = "Diego", Last = "Vega" }); + #endregion + } + } +} + + diff --git a/MicroRuleEngine.Core.Tests/Models/Order.cs b/MicroRuleEngine.Core.Tests/Models/Order.cs new file mode 100644 index 0000000..453d924 --- /dev/null +++ b/MicroRuleEngine.Core.Tests/Models/Order.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MicroRuleEngine.Core.Tests.Models +{ + public enum Status + { + Open, + Cancelled, + Completed + }; + + public class Order + { + public Order() + { + Items = new List(); + } + public int OrderId { get; set; } + public Customer Customer { get; set; } + public List Items { get; set; } + + public List Codes { get; set; } + public decimal? Total { get; set; } + public DateTime OrderDate { get; set; } + public bool HasItem(string itemCode) + { + return Items.Any(x => x.ItemCode == itemCode); + } + + public Status Status { get; set; } + + } + + public class Item + { + public decimal Cost { get; set; } + public string ItemCode { get; set; } + } + + public class Customer + { + public string FirstName { get; set; } + public string LastName { get; set; } + public Country Country { get; set; } + } + + public class Country + { + public string CountryCode { get; set; } + } +} diff --git a/MicroRuleEngine.Tests/ExampleUsage.cs b/MicroRuleEngine.Tests/ExampleUsage.cs index 84fa254..c736190 100644 --- a/MicroRuleEngine.Tests/ExampleUsage.cs +++ b/MicroRuleEngine.Tests/ExampleUsage.cs @@ -232,6 +232,12 @@ public static Order GetOrder() new Item { ItemCode = "LD45", Cost=5.25M}, new Item { ItemCode = "Test", Cost=3.33M}, }, + Codes = new List() + { + 555, + 321, + 243 + }, Total = 13.83m, OrderDate = new DateTime(1776, 7, 4), Status = Status.Open diff --git a/MicroRuleEngine.Tests/Models/Order.cs b/MicroRuleEngine.Tests/Models/Order.cs index 1ea9d85..77c20d8 100644 --- a/MicroRuleEngine.Tests/Models/Order.cs +++ b/MicroRuleEngine.Tests/Models/Order.cs @@ -28,7 +28,7 @@ public bool HasItem(string itemCode) } public Status Status { get; set; } - + public List Codes { get; set; } } public class Item diff --git a/MicroRuleEngine.Tests/SerializationTests.cs b/MicroRuleEngine.Tests/SerializationTests.cs index 72109c5..dbcafae 100644 --- a/MicroRuleEngine.Tests/SerializationTests.cs +++ b/MicroRuleEngine.Tests/SerializationTests.cs @@ -34,7 +34,7 @@ public void XmlSerialization() using (var reader = new StringReader(text)) using (var xr = XmlReader.Create(reader)) { - newRule = (Rule) serializer.ReadObject(xr); + newRule = (Rule)serializer.ReadObject(xr); } var order = ExampleUsage.GetOrder(); @@ -75,7 +75,7 @@ public void JsonSerialization() using (var stream2 = new MemoryStream(Encoding.UTF8.GetBytes(text))) { - newRule = (Rule) serializer.ReadObject(stream2); + newRule = (Rule)serializer.ReadObject(stream2); } var order = ExampleUsage.GetOrder(); @@ -90,5 +90,25 @@ public void JsonSerialization() Assert.IsFalse(passes); } + [TestMethod] + public void JsonVisualizationTest() + { + var order = ExampleUsage.GetOrder(); + var members = MRE.Member.GetFields(order.GetType()); + var serializer = new DataContractJsonSerializer(members.GetType()); + + string text; + + using (var stream1 = new MemoryStream()) + { + + serializer.WriteObject(stream1, members); + + stream1.Position = 0; + var sr = new StreamReader(stream1); + text = sr.ReadToEnd(); + } + Assert.IsTrue(text.Length > 100); + } } } diff --git a/MicroRuleEngine.sln b/MicroRuleEngine.sln index 3ce0a43..d1331c4 100644 --- a/MicroRuleEngine.sln +++ b/MicroRuleEngine.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27004.2008 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29230.47 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroRuleEngine.Tests", "MicroRuleEngine.Tests\MicroRuleEngine.Tests.csproj", "{392E9585-525F-4B82-9DA0-C4AC2812C558}" EndProject @@ -13,7 +13,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution TraceAndTestImpact.testsettings = TraceAndTestImpact.testsettings EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroRuleEngine", "MicroRuleEngine\MicroRuleEngine.csproj", "{C6F8FBCE-8949-455A-AE4A-20A2B2012AEE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroRuleEngine", "MicroRuleEngine\MicroRuleEngine.csproj", "{C6F8FBCE-8949-455A-AE4A-20A2B2012AEE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroRuleEngine.Core.Tests", "MicroRuleEngine.Core.Tests\MicroRuleEngine.Core.Tests.csproj", "{B070AE15-A3F8-4B33-9FC0-B2BB4C989A45}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -29,6 +31,10 @@ Global {C6F8FBCE-8949-455A-AE4A-20A2B2012AEE}.Debug|Any CPU.Build.0 = Debug|Any CPU {C6F8FBCE-8949-455A-AE4A-20A2B2012AEE}.Release|Any CPU.ActiveCfg = Release|Any CPU {C6F8FBCE-8949-455A-AE4A-20A2B2012AEE}.Release|Any CPU.Build.0 = Release|Any CPU + {B070AE15-A3F8-4B33-9FC0-B2BB4C989A45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B070AE15-A3F8-4B33-9FC0-B2BB4C989A45}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B070AE15-A3F8-4B33-9FC0-B2BB4C989A45}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B070AE15-A3F8-4B33-9FC0-B2BB4C989A45}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MicroRuleEngine/MRE.cs b/MicroRuleEngine/MRE.cs index a36b05d..53857bc 100644 --- a/MicroRuleEngine/MRE.cs +++ b/MicroRuleEngine/MRE.cs @@ -9,557 +9,782 @@ namespace MicroRuleEngine { - public class MRE - { - private static readonly ExpressionType[] _nestedOperators = new ExpressionType[] - {ExpressionType.And, ExpressionType.AndAlso, ExpressionType.Or, ExpressionType.OrElse}; + public class MRE + { + private static readonly ExpressionType[] _nestedOperators = new ExpressionType[] + {ExpressionType.And, ExpressionType.AndAlso, ExpressionType.Or, ExpressionType.OrElse}; - private static readonly Lazy _miRegexIsMatch = new Lazy(() => - typeof(Regex).GetMethod("IsMatch", new[] {typeof(string), typeof(string), typeof(RegexOptions)})); + private static readonly Lazy _miRegexIsMatch = new Lazy(() => + typeof(Regex).GetMethod("IsMatch", new[] { typeof(string), typeof(string), typeof(RegexOptions) })); - private static readonly Lazy _miGetItem = new Lazy(() => - typeof(System.Data.DataRow).GetMethod("get_Item", new Type[] {typeof(string)})); + private static readonly Lazy _miGetItem = new Lazy(() => + typeof(System.Data.DataRow).GetMethod("get_Item", new Type[] { typeof(string) })); - private static readonly Tuple>[] _enumrMethodsByName = - new Tuple>[] - { - Tuple.Create("Any", new Lazy(() => GetLinqMethod("Any", 2))), - Tuple.Create("All", new Lazy(() => GetLinqMethod("All", 2))), - }; - private static readonly Lazy _miIntTryParse = new Lazy(() => - typeof(Int32).GetMethod("TryParse", new Type[] { typeof(string), Type.GetType("System.Int32&") })); + private static readonly Tuple>[] _enumrMethodsByName = + new Tuple>[] + { + Tuple.Create("Any", new Lazy(() => GetLinqMethod("Any", 2))), + Tuple.Create("All", new Lazy(() => GetLinqMethod("All", 2))), + }; + private static readonly Lazy _miIntTryParse = new Lazy(() => + typeof(Int32).GetMethod("TryParse", new Type[] { typeof(string), Type.GetType("System.Int32&") })); - private static readonly Lazy _miFloatTryParse = new Lazy(() => - typeof(Single).GetMethod("TryParse", new Type[] { typeof(string), Type.GetType("System.Single&") })); + private static readonly Lazy _miFloatTryParse = new Lazy(() => + typeof(Single).GetMethod("TryParse", new Type[] { typeof(string), Type.GetType("System.Single&") })); - private static readonly Lazy _miDoubleTryParse = new Lazy(() => - typeof(Double).GetMethod("TryParse", new Type[] { typeof(string), Type.GetType("System.Double&") })); + private static readonly Lazy _miDoubleTryParse = new Lazy(() => + typeof(Double).GetMethod("TryParse", new Type[] { typeof(string), Type.GetType("System.Double&") })); - private static readonly Lazy _miDecimalTryParse = new Lazy(() => - typeof(Decimal).GetMethod("TryParse", new Type[] { typeof(string), Type.GetType("System.Decimal&") })); + private static readonly Lazy _miDecimalTryParse = new Lazy(() => + typeof(Decimal).GetMethod("TryParse", new Type[] { typeof(string), Type.GetType("System.Decimal&") })); public Func CompileRule(Rule r) - { - var paramUser = Expression.Parameter(typeof(T)); - Expression expr = GetExpressionForRule(typeof(T), r, paramUser); - - return Expression.Lambda>(expr, paramUser).Compile(); - } - - public Func CompileRule(Type type, Rule r) - { - var paramUser = Expression.Parameter(typeof(object)); - Expression expr = GetExpressionForRule(type, r, paramUser); - - return Expression.Lambda>(expr, paramUser).Compile(); - } - - public Func CompileRules(IEnumerable rules) - { - var paramUser = Expression.Parameter(typeof(T)); - var expr = BuildNestedExpression(typeof(T), rules, paramUser, ExpressionType.And); - return Expression.Lambda>(expr, paramUser).Compile(); - } - - public Func CompileRules(Type type, IEnumerable rules) - { - var paramUser = Expression.Parameter(type); - var expr = BuildNestedExpression(type, rules, paramUser, ExpressionType.And); - return Expression.Lambda>(expr, paramUser).Compile(); - } - - // Build() in some forks - protected static Expression GetExpressionForRule(Type type, Rule rule, ParameterExpression parameterExpression) - { - ExpressionType nestedOperator; - if (ExpressionType.TryParse(rule.Operator, out nestedOperator) && - _nestedOperators.Contains(nestedOperator) && rule.Rules != null && rule.Rules.Any()) - return BuildNestedExpression(type, rule.Rules, parameterExpression, nestedOperator); - else - return BuildExpr(type, rule, parameterExpression); - } - - protected static Expression BuildNestedExpression(Type type, IEnumerable rules, ParameterExpression param, - ExpressionType operation) - { - var expressions = rules.Select(r => GetExpressionForRule(type, r, param)); - return BinaryExpression(expressions, operation); - } - - protected static Expression BinaryExpression(IEnumerable expressions, ExpressionType operationType) - { - Func methodExp; - switch (operationType) - { - case ExpressionType.Or: - methodExp = Expression.Or; - break; - case ExpressionType.OrElse: - methodExp = Expression.OrElse; - break; - case ExpressionType.AndAlso: - methodExp = Expression.AndAlso; - break; - default: - case ExpressionType.And: - methodExp = Expression.And; - break; - } - - return expressions.Aggregate(methodExp); - } - - static readonly Regex _regexIndexed = new Regex(@"(\w+)\[(\d+)\]", RegexOptions.Compiled); - - private static Expression GetProperty(Expression param, string propname) - { - Expression propExpression = param; - String[] childProperties = propname.Split('.'); - var propertyType = param.Type; - - foreach (var childprop in childProperties) - { - var isIndexed = _regexIndexed.Match(childprop); - if (isIndexed.Success) - { - var collectionname = isIndexed.Groups[1].Value; - var index = Int32.Parse(isIndexed.Groups[2].Value); - var collexpr = GetProperty(param, collectionname); - var collectionType = collexpr.Type; - if (collectionType.IsArray) - { - propExpression = Expression.ArrayAccess(collexpr, Expression.Constant(index)); - propertyType = propExpression.Type; - } - else - { - var getter = collectionType.GetMethod("get_Item", new Type[] {typeof(Int32)}); - if (getter==null) - throw new RulesException($"'{collectionname} ({collectionType.Name}) cannot be indexed"); - propExpression = Expression.Call(collexpr, getter, Expression.Constant(index)); - propertyType = getter.ReturnType; - } - } - else - { - var property = propertyType.GetProperty(childprop); - if (property == null) - throw new RulesException( - $"Cannot find property {childprop} in class {propertyType.Name} (\"{propname}\")"); - propExpression = Expression.PropertyOrField(propExpression, childprop); - propertyType = property.PropertyType; - } - } - - return propExpression; - } - - private static Expression BuildEnumerableOperatorExpression(Type type, Rule rule, - ParameterExpression parameterExpression) - { - var collectionPropertyExpression = BuildExpr(type, rule, parameterExpression); - - var itemType = GetCollectionItemType(collectionPropertyExpression.Type); - var expressionParameter = Expression.Parameter(itemType); - - - var genericFunc = typeof(Func<,>).MakeGenericType(itemType, typeof(bool)); - - var innerExp = BuildNestedExpression(itemType, rule.Rules, expressionParameter, ExpressionType.And); - var predicate = Expression.Lambda(genericFunc, innerExp, expressionParameter); - - var body = Expression.Call(typeof(Enumerable), rule.Operator, new[] {itemType}, - collectionPropertyExpression, predicate); - - return body; - } - - private static Type GetCollectionItemType(Type collectionType) - { - if (collectionType.IsArray) - return collectionType.GetElementType(); - - if ((collectionType.GetInterface("IEnumerable") != null)) - return collectionType.GetGenericArguments()[0]; - - return typeof(object); - } - - - private static MethodInfo IsEnumerableOperator(string oprator) - { - return (from tup in _enumrMethodsByName - where string.Equals(oprator, tup.Item1, StringComparison.CurrentCultureIgnoreCase) - select tup.Item2.Value).FirstOrDefault(); - } - - private static Expression BuildExpr(Type type, Rule rule, Expression param) - { - Expression propExpression; - Type propType; - - if (param.Type == typeof(object)) - { - param = Expression.TypeAs(param, type); - } - var drule = rule as DataRule; - - if (string.IsNullOrEmpty(rule.MemberName)) //check is against the object itself - { - propExpression = param; - propType = propExpression.Type; - } - else if (drule != null) - { - if (type != typeof(System.Data.DataRow)) - throw new RulesException(" Bad rule"); - propExpression = GetDataRowField(param, drule.MemberName, drule.Type); - propType = propExpression.Type; - } - else - { - propExpression = GetProperty(param, rule.MemberName); - propType = propExpression.Type; - } - - propExpression = Expression.TryCatch( - Expression.Block(propExpression.Type, propExpression), - Expression.Catch(typeof(NullReferenceException), Expression.Default(propExpression.Type)) - ); - // is the operator a known .NET operator? - ExpressionType tBinary; - - if (ExpressionType.TryParse(rule.Operator, out tBinary)) - { - Expression right; - var txt = rule.TargetValue as string; - if (txt != null && txt.StartsWith("*.")) - { - txt = txt.Substring(2); - right = GetProperty(param, txt); - } - else - right = StringToExpression(rule.TargetValue, propType); - - return Expression.MakeBinary(tBinary, propExpression, right); - } - - switch (rule.Operator) - { - case "IsMatch": - return Expression.Call( - _miRegexIsMatch.Value, - propExpression, - Expression.Constant(rule.TargetValue, typeof(string)), - Expression.Constant(RegexOptions.IgnoreCase, typeof(RegexOptions)) - ); - case "IsInteger": - return Expression.Call( - _miIntTryParse.Value, - propExpression, - Expression.MakeMemberAccess(null, typeof(Placeholder).GetField("Int")) - ); - case "IsSingle": - return Expression.Call( - _miFloatTryParse.Value, - propExpression, - Expression.MakeMemberAccess(null, typeof(Placeholder).GetField("Float")) - ); - case "IsDouble": - return Expression.Call( - _miDoubleTryParse.Value, - propExpression, - Expression.MakeMemberAccess(null, typeof(Placeholder).GetField("Double")) - ); - case "IsDecimal": - return Expression.Call( - _miDecimalTryParse.Value, - propExpression, - Expression.MakeMemberAccess(null, typeof(Placeholder).GetField("Decimal")) - ); + { + var paramUser = Expression.Parameter(typeof(T)); + Expression expr = GetExpressionForRule(typeof(T), r, paramUser); + + return Expression.Lambda>(expr, paramUser).Compile(); + } + public static Expression> ToExpression(Rule r, bool useTryCatchForNulls = true) + { + var paramUser = Expression.Parameter(typeof(T)); + Expression expr = GetExpressionForRule(typeof(T), r, paramUser, useTryCatchForNulls); + + return Expression.Lambda>(expr, paramUser); + } + + public static Func ToFunc(Rule r, bool useTryCatchForNulls = true) + { + return ToExpression(r, useTryCatchForNulls).Compile(); + } + public static Expression> ToExpression(Type type, Rule r) + { + var paramUser = Expression.Parameter(typeof(object)); + Expression expr = GetExpressionForRule(type, r, paramUser); + + return Expression.Lambda>(expr, paramUser); + } + + public static Func ToFunc(Type type, Rule r) + { + return ToExpression(type, r).Compile(); + } + + public Func CompileRule(Type type, Rule r) + { + var paramUser = Expression.Parameter(typeof(object)); + Expression expr = GetExpressionForRule(type, r, paramUser); + + return Expression.Lambda>(expr, paramUser).Compile(); + } + + public Func CompileRules(IEnumerable rules) + { + var paramUser = Expression.Parameter(typeof(T)); + var expr = BuildNestedExpression(typeof(T), rules, paramUser, ExpressionType.And); + return Expression.Lambda>(expr, paramUser).Compile(); + } + + public Func CompileRules(Type type, IEnumerable rules) + { + var paramUser = Expression.Parameter(type); + var expr = BuildNestedExpression(type, rules, paramUser, ExpressionType.And); + return Expression.Lambda>(expr, paramUser).Compile(); + } + + // Build() in some forks + protected static Expression GetExpressionForRule(Type type, Rule rule, ParameterExpression parameterExpression, bool useTryCatchForNulls = true) + { + ExpressionType nestedOperator; + if (ExpressionType.TryParse(rule.Operator, out nestedOperator) && + _nestedOperators.Contains(nestedOperator) && rule.Rules != null && rule.Rules.Any()) + return BuildNestedExpression(type, rule.Rules, parameterExpression, nestedOperator, useTryCatchForNulls); + else + return BuildExpr(type, rule, parameterExpression, useTryCatchForNulls); + } + + protected static Expression BuildNestedExpression(Type type, IEnumerable rules, ParameterExpression param, + ExpressionType operation, bool useTryCatchForNulls = true) + { + var expressions = rules.Select(r => GetExpressionForRule(type, r, param, useTryCatchForNulls)); + return BinaryExpression(expressions, operation); + } + + protected static Expression BinaryExpression(IEnumerable expressions, ExpressionType operationType) + { + Func methodExp; + switch (operationType) + { + case ExpressionType.Or: + methodExp = Expression.Or; + break; + case ExpressionType.OrElse: + methodExp = Expression.OrElse; + break; + case ExpressionType.AndAlso: + methodExp = Expression.AndAlso; + break; default: + case ExpressionType.And: + methodExp = Expression.And; break; - } + } + + return expressions.Aggregate(methodExp); + } + + static readonly Regex _regexIndexed = new Regex(@"(\w+)\[(\d+)\]", RegexOptions.Compiled); + + private static Expression GetProperty(Expression param, string propname) + { + Expression propExpression = param; + String[] childProperties = propname.Split('.'); + var propertyType = param.Type; + + foreach (var childprop in childProperties) + { + var isIndexed = _regexIndexed.Match(childprop); + if (isIndexed.Success) + { + var collectionname = isIndexed.Groups[1].Value; + var index = Int32.Parse(isIndexed.Groups[2].Value); + var collectionProp = propertyType.GetProperty(collectionname); + if (collectionProp == null) + throw new RulesException( + $"Cannot find collection property {collectionname} in class {propertyType.Name} (\"{propname}\")"); + var collexpr = Expression.PropertyOrField(propExpression, collectionname); + + var collectionType = collexpr.Type; + if (collectionType.IsArray) + { + propExpression = Expression.ArrayAccess(collexpr, Expression.Constant(index)); + propertyType = propExpression.Type; + } + else + { + var getter = collectionType.GetMethod("get_Item", new Type[] { typeof(Int32) }); + if (getter == null) + throw new RulesException($"'{collectionname} ({collectionType.Name}) cannot be indexed"); + propExpression = Expression.Call(collexpr, getter, Expression.Constant(index)); + propertyType = getter.ReturnType; + } + } + else + { + var property = propertyType.GetProperty(childprop); + if (property == null) + throw new RulesException( + $"Cannot find property {childprop} in class {propertyType.Name} (\"{propname}\")"); + propExpression = Expression.PropertyOrField(propExpression, childprop); + propertyType = property.PropertyType; + } + } + + return propExpression; + } + + private static Expression BuildEnumerableOperatorExpression(Type type, Rule rule, + ParameterExpression parameterExpression) + { + var collectionPropertyExpression = BuildExpr(type, rule, parameterExpression); + + var itemType = GetCollectionItemType(collectionPropertyExpression.Type); + var expressionParameter = Expression.Parameter(itemType); + + + var genericFunc = typeof(Func<,>).MakeGenericType(itemType, typeof(bool)); + + var innerExp = BuildNestedExpression(itemType, rule.Rules, expressionParameter, ExpressionType.And); + var predicate = Expression.Lambda(genericFunc, innerExp, expressionParameter); + + var body = Expression.Call(typeof(Enumerable), rule.Operator, new[] { itemType }, + collectionPropertyExpression, predicate); + + return body; + } + + private static Type GetCollectionItemType(Type collectionType) + { + if (collectionType.IsArray) + return collectionType.GetElementType(); + + if ((collectionType.GetInterface("IEnumerable") != null)) + return collectionType.GetGenericArguments()[0]; + + return typeof(object); + } + + + private static MethodInfo IsEnumerableOperator(string oprator) + { + return (from tup in _enumrMethodsByName + where string.Equals(oprator, tup.Item1, StringComparison.CurrentCultureIgnoreCase) + select tup.Item2.Value).FirstOrDefault(); + } + + private static Expression BuildExpr(Type type, Rule rule, Expression param, bool useTryCatch = true) + { + Expression propExpression; + Type propType; + + if (param.Type == typeof(object)) + { + param = Expression.TypeAs(param, type); + } + var drule = rule as DataRule; + + if (string.IsNullOrEmpty(rule.MemberName)) //check is against the object itself + { + propExpression = param; + propType = propExpression.Type; + } + else if (drule != null) + { + if (type != typeof(System.Data.DataRow)) + throw new RulesException(" Bad rule"); + propExpression = GetDataRowField(param, drule.MemberName, drule.Type); + propType = propExpression.Type; + } + else + { + propExpression = GetProperty(param, rule.MemberName); + propType = propExpression.Type; + } + if (useTryCatch) + { + propExpression = Expression.TryCatch( + Expression.Block(propExpression.Type, propExpression), + Expression.Catch(typeof(NullReferenceException), Expression.Default(propExpression.Type)) + ); + } + + // is the operator a known .NET operator? + ExpressionType tBinary; + + if (ExpressionType.TryParse(rule.Operator, out tBinary)) + { + Expression right; + var txt = rule.TargetValue as string; + if (txt != null && txt.StartsWith("*.")) + { + txt = txt.Substring(2); + right = GetProperty(param, txt); + } + else + right = StringToExpression(rule.TargetValue, propType); + + return Expression.MakeBinary(tBinary, propExpression, right); + } + + switch (rule.Operator) + { + case "IsMatch": + return Expression.Call( + _miRegexIsMatch.Value, + propExpression, + Expression.Constant(rule.TargetValue, typeof(string)), + Expression.Constant(RegexOptions.IgnoreCase, typeof(RegexOptions)) + ); + case "IsInteger": + return Expression.Call( + _miIntTryParse.Value, + propExpression, + Expression.MakeMemberAccess(null, typeof(Placeholder).GetField("Int")) + ); + case "IsSingle": + return Expression.Call( + _miFloatTryParse.Value, + propExpression, + Expression.MakeMemberAccess(null, typeof(Placeholder).GetField("Float")) + ); + case "IsDouble": + return Expression.Call( + _miDoubleTryParse.Value, + propExpression, + Expression.MakeMemberAccess(null, typeof(Placeholder).GetField("Double")) + ); + case "IsDecimal": + return Expression.Call( + _miDecimalTryParse.Value, + propExpression, + Expression.MakeMemberAccess(null, typeof(Placeholder).GetField("Decimal")) + ); + default: + break; + } var enumrOperation = IsEnumerableOperator(rule.Operator); - if (enumrOperation != null) - { - var elementType = ElementType(propType); - var lambdaParam = Expression.Parameter(elementType, "lambdaParam"); - return rule.Rules?.Any() == true - ? Expression.Call(enumrOperation.MakeGenericMethod(elementType), - propExpression, - Expression.Lambda( - BuildNestedExpression(elementType, rule.Rules, lambdaParam, ExpressionType.AndAlso), - lambdaParam) - - - ) - : Expression.Call(enumrOperation.MakeGenericMethod(elementType), propExpression); - } - else //Invoke a method on the Property - { - var inputs = rule.Inputs.Select(x => x.GetType()).ToArray(); - var methodInfo = propType.GetMethod(rule.Operator, inputs); - if (methodInfo == null) - throw new RulesException($"'{rule.Operator}' is not a method of '{propType.Name}"); - - if (!methodInfo.IsGenericMethod) - inputs = null; //Only pass in type information to a Generic Method - var expressions = rule.Inputs.Select(Expression.Constant).ToArray(); - - return Expression.TryCatch( - Expression.Block(typeof(bool), Expression.Call(propExpression, rule.Operator, inputs, expressions)), - Expression.Catch(typeof(NullReferenceException), Expression.Constant(false)) - ); - } - } - - private static MethodInfo GetLinqMethod(string name, int numParameter) - { - return typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public) - .FirstOrDefault(m => m.Name == name && m.GetParameters().Length == numParameter); - } - - - private static Expression GetDataRowField(Expression prm, string member, string typeName) - { - var expMember = Expression.Call(prm, _miGetItem.Value, Expression.Constant(member, typeof(string))); - var type = Type.GetType(typeName); - Debug.Assert(type != null); - - if (type.IsClass || typeName.StartsWith("System.Nullable") ) - { - // equals "return testValue == DBNull.Value ? (typeName) null : (typeName) testValue" - return Expression.Condition(Expression.Equal(expMember, Expression.Constant(DBNull.Value)), - Expression.Constant(null, type), - Expression.Convert(expMember, type)); - } - else - // equals "return (typeName) testValue" - return Expression.Convert(expMember, type); - } - - private static Expression StringToExpression(object value, Type propType) - { - Debug.Assert(propType != null); - - object safevalue; - Type valuetype = propType; - var txt = value as string; - if (value == null) - { - safevalue = null; - } - else if (txt != null) - { - if (txt.ToLower() == "null") - safevalue = null; - else if (propType.IsEnum) - safevalue = Enum.Parse(propType, txt); - else - safevalue = Convert.ChangeType(value, valuetype); - } - else if (propType.Name == "Nullable`1") - { - valuetype = Nullable.GetUnderlyingType(propType); - safevalue = Convert.ChangeType(value, valuetype); - } - else - safevalue = Convert.ChangeType(value, valuetype); - - return Expression.Constant(safevalue, propType); - } - - private static Type ElementType(Type seqType) - { - Type ienum = FindIEnumerable(seqType); - if (ienum == null) return seqType; - return ienum.GetGenericArguments()[0]; - } - - private static Type FindIEnumerable(Type seqType) - { - if (seqType == null || seqType == typeof(string)) - return null; - if (seqType.IsArray) - return typeof(IEnumerable<>).MakeGenericType(seqType.GetElementType()); - if (seqType.IsGenericType) - { - foreach (Type arg in seqType.GetGenericArguments()) - { - Type ienum = typeof(IEnumerable<>).MakeGenericType(arg); - if (ienum.IsAssignableFrom(seqType)) - { - return ienum; - } - } - } - - Type[] ifaces = seqType.GetInterfaces(); - foreach (Type iface in ifaces) - { - Type ienum = FindIEnumerable(iface); - if (ienum != null) - return ienum; - } - - if (seqType.BaseType != null && seqType.BaseType != typeof(object)) - { - return FindIEnumerable(seqType.BaseType); - } - - return null; - } - } - - [DataContract] - public class Rule - { - public Rule() - { - Inputs = Enumerable.Empty(); - } - - [DataMember] public string MemberName { get; set; } - [DataMember] public string Operator { get; set; } - [DataMember] public object TargetValue { get; set; } - [DataMember] public IList Rules { get; set; } - [DataMember] public IEnumerable Inputs { get; set; } - - - public static Rule operator |(Rule lhs, Rule rhs) - { - var rule = new Rule {Operator = "Or"}; - return MergeRulesInto(rule, lhs, rhs); - } - - public static Rule operator &(Rule lhs, Rule rhs) - { - var rule = new Rule {Operator = "AndAlso"}; - return MergeRulesInto(rule, lhs, rhs); - } - - private static Rule MergeRulesInto(Rule target, Rule lhs, Rule rhs) - { - target.Rules = new List(); - - if (lhs.Rules != null && lhs.Operator == target.Operator) // left is multiple - { - target.Rules.AddRange(lhs.Rules); - if (rhs.Rules != null && rhs.Operator == target.Operator) - target.Rules.AddRange(rhs.Rules); // left & right are multiple - else - target.Rules.Add(rhs); // left multi, right single - } - else if (rhs.Rules != null && rhs.Operator == target.Operator) - { - target.Rules.Add(lhs); // left single, right multi - target.Rules.AddRange(rhs.Rules); - } - else - { - target.Rules.Add(lhs); - target.Rules.Add(rhs); - } - - - return target; - } - - public static Rule Create(string member, mreOperator oper, object target) - { - return new Rule {MemberName = member, TargetValue = target, Operator = oper.ToString()}; - } - - public static Rule MethodOnChild(string member, string methodName, params object[] inputs) - { - return new Rule {MemberName = member, Inputs = inputs.ToList(), Operator = methodName}; - } - - public static Rule Method(string methodName, params object[] inputs) - { - return new Rule {Inputs = inputs.ToList(), Operator = methodName}; - } - - public static Rule Any(string member, Rule rule) - { - return new Rule {MemberName = member, Operator = "Any", Rules = new List {rule}}; - } - - public static Rule All(string member, Rule rule) - { - return new Rule {MemberName = member, Operator = "All", Rules = new List {rule}}; - } - - public static Rule IsInteger(string member) => new Rule() {MemberName = member, Operator = "IsInteger"}; - public static Rule IsFloat(string member) => new Rule() { MemberName = member, Operator = "IsSingle" }; - public static Rule IsDouble(string member) => new Rule() { MemberName = member, Operator = "IsDouble" }; - public static Rule IsSingle(string member) => new Rule() { MemberName = member, Operator = "IsSingle" }; - public static Rule IsDecimal(string member) => new Rule() { MemberName = member, Operator = "IsDecimal" }; + if (enumrOperation != null) + { + var elementType = ElementType(propType); + var lambdaParam = Expression.Parameter(elementType, "lambdaParam"); + return rule.Rules?.Any() == true + ? Expression.Call(enumrOperation.MakeGenericMethod(elementType), + propExpression, + Expression.Lambda( + BuildNestedExpression(elementType, rule.Rules, lambdaParam, ExpressionType.AndAlso), + lambdaParam) + + + ) + : Expression.Call(enumrOperation.MakeGenericMethod(elementType), propExpression); + } + else //Invoke a method on the Property + { + var inputs = rule.Inputs.Select(x => x.GetType()).ToArray(); + var methodInfo = propType.GetMethod(rule.Operator, inputs); + List expressions = new List(); + + if (methodInfo == null) + { + methodInfo = propType.GetMethod(rule.Operator); + if (methodInfo != null) + { + var parameters = methodInfo.GetParameters(); + int i = 0; + foreach (var item in rule.Inputs) + { + expressions.Add(MRE.StringToExpression(item, parameters[i].ParameterType)); + i++; + } + } + } + else + expressions.AddRange(rule.Inputs.Select(Expression.Constant)); + if (methodInfo == null) + throw new RulesException($"'{rule.Operator}' is not a method of '{propType.Name}"); + + + if (!methodInfo.IsGenericMethod) + inputs = null; //Only pass in type information to a Generic Method + var callExpression = Expression.Call(propExpression, rule.Operator, inputs, expressions.ToArray()); + if (useTryCatch) + { + return Expression.TryCatch( + Expression.Block(typeof(bool), callExpression), + Expression.Catch(typeof(NullReferenceException), Expression.Constant(false)) + ); + } + else + return callExpression; + } + } + + private static MethodInfo GetLinqMethod(string name, int numParameter) + { + return typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public) + .FirstOrDefault(m => m.Name == name && m.GetParameters().Length == numParameter); + } + + + private static Expression GetDataRowField(Expression prm, string member, string typeName) + { + var expMember = Expression.Call(prm, _miGetItem.Value, Expression.Constant(member, typeof(string))); + var type = Type.GetType(typeName); + Debug.Assert(type != null); + + if (type.IsClass || typeName.StartsWith("System.Nullable")) + { + // equals "return testValue == DBNull.Value ? (typeName) null : (typeName) testValue" + return Expression.Condition(Expression.Equal(expMember, Expression.Constant(DBNull.Value)), + Expression.Constant(null, type), + Expression.Convert(expMember, type)); + } + else + // equals "return (typeName) testValue" + return Expression.Convert(expMember, type); + } + + private static Expression StringToExpression(object value, Type propType) + { + Debug.Assert(propType != null); + + object safevalue; + Type valuetype = propType; + var txt = value as string; + if (value == null) + { + safevalue = null; + } + else if (txt != null) + { + if (txt.ToLower() == "null") + safevalue = null; + else if (propType.IsEnum) + safevalue = Enum.Parse(propType, txt); + else if (propType.Name == "Nullable`1") + { + valuetype = Nullable.GetUnderlyingType(propType); + safevalue = Convert.ChangeType(value, valuetype); + } + else + safevalue = Convert.ChangeType(value, valuetype); + } + else if (propType.Name == "Nullable`1") + { + valuetype = Nullable.GetUnderlyingType(propType); + safevalue = Convert.ChangeType(value, valuetype); + } + else + safevalue = Convert.ChangeType(value, valuetype); + + return Expression.Constant(safevalue, propType); + } + + private static Type ElementType(Type seqType) + { + Type ienum = FindIEnumerable(seqType); + if (ienum == null) return seqType; + return ienum.GetGenericArguments()[0]; + } + + private static Type FindIEnumerable(Type seqType) + { + if (seqType == null || seqType == typeof(string)) + return null; + if (seqType.IsArray) + return typeof(IEnumerable<>).MakeGenericType(seqType.GetElementType()); + if (seqType.IsGenericType) + { + foreach (Type arg in seqType.GetGenericArguments()) + { + Type ienum = typeof(IEnumerable<>).MakeGenericType(arg); + if (ienum.IsAssignableFrom(seqType)) + { + return ienum; + } + } + } + + Type[] ifaces = seqType.GetInterfaces(); + foreach (Type iface in ifaces) + { + Type ienum = FindIEnumerable(iface); + if (ienum != null) + return ienum; + } + + if (seqType.BaseType != null && seqType.BaseType != typeof(object)) + { + return FindIEnumerable(seqType.BaseType); + } + + return null; + } + public enum OperatorType + { + InternalString = 1, + ObjectMethod = 2, + Comparison = 3, + Logic = 4 + } + public class Operator + { + public string Name { get; set; } + public OperatorType Type { get; set; } + public int NumberOfInputs { get; set; } + public bool SimpleInputs { get; set; } + } + public class Member + { + public string Name { get; set; } + public string Type { get; set; } + public List PossibleOperators { get; set; } + public static bool IsSimpleType(Type type) + { + return + type.IsPrimitive || + new Type[] { + typeof(Enum), + typeof(String), + typeof(Decimal), + typeof(DateTime), + typeof(DateTimeOffset), + typeof(TimeSpan), + typeof(Guid) + }.Contains(type) || + Convert.GetTypeCode(type) != TypeCode.Object || + (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>) && IsSimpleType(type.GetGenericArguments()[0])) + ; + } + public static BindingFlags flags = BindingFlags.Instance | BindingFlags.Public; + public static List GetFields(System.Type type, string memberName = null, string parentPath = null) + { + List toReturn = new List(); + var fi = new Member(); + fi.Name = string.IsNullOrEmpty(parentPath) ? memberName : $"{parentPath}.{memberName}"; + fi.Type = type.ToString(); + fi.PossibleOperators = Member.Operators(type, string.IsNullOrEmpty(fi.Name)); + toReturn.Add(fi); + if (!Member.IsSimpleType(type)) + { + var fields = type.GetFields(Member.flags); + var properties = type.GetProperties(Member.flags); + foreach (var field in fields) + { + string useParentName = null; + var name = Member.ValidateName(field.Name, type, memberName, fi.Name, parentPath, out useParentName); + toReturn.AddRange(GetFields(field.FieldType, name, useParentName)); + } + foreach (var prop in properties) + { + string useParentName = null; + var name = Member.ValidateName(prop.Name, type, memberName, fi.Name, parentPath, out useParentName); + toReturn.AddRange(GetFields(prop.PropertyType, name, useParentName)); + } + } + return toReturn; + } + private static string ValidateName(string name, Type parentType, string parentName, string parentPath, string grandparentPath, out string useAsParentPath) + { + if (name == "Item" && IsGenericList(parentType)) + { + useAsParentPath = grandparentPath; + return parentName + "[0]"; + } + else + { + useAsParentPath = parentPath; + return name; + } + } + public static bool IsGenericList(Type type) + { + if (type == null) + { + throw new ArgumentNullException("type"); + } + foreach (Type @interface in type.GetInterfaces()) + { + if (@interface.IsGenericType) + { + if (@interface.GetGenericTypeDefinition() == typeof(ICollection<>)) + { + // if needed, you can also return the type used as generic argument + return true; + } + } + } + return false; + } + private static string[] logicOperators = new string[] { + mreOperator.And.ToString("g"), + mreOperator.AndAlso.ToString("g"), + mreOperator.Or.ToString("g"), + mreOperator.OrElse.ToString("g") + }; + private static string[] comparisonOperators = new string[] { + mreOperator.Equal.ToString("g"), + mreOperator.GreaterThan.ToString("g"), + mreOperator.GreaterThanOrEqual.ToString("g"), + mreOperator.LessThan.ToString("g"), + mreOperator.LessThanOrEqual.ToString("g"), + mreOperator.NotEqual.ToString("g"), + }; + + private static string[] hardCodedStringOperators = new string[] { + mreOperator.IsMatch.ToString("g"), + mreOperator.IsInteger.ToString("g"), + mreOperator.IsSingle.ToString("g"), + mreOperator.IsDouble.ToString("g"), + mreOperator.IsDecimal.ToString("g") + }; + public static List Operators(System.Type type, bool addLogicOperators = false, bool noOverloads = true) + { + List operators = new List(); + if (addLogicOperators) + { + operators.AddRange(logicOperators.Select(x => new Operator() { Name = x, Type = OperatorType.Logic })); + } + + if (type == typeof(String)) + { + operators.AddRange(hardCodedStringOperators.Select(x => new Operator() { Name = x, Type = OperatorType.InternalString })); + } + else if (Member.IsSimpleType(type)) + { + operators.AddRange(comparisonOperators.Select(x => new Operator() { Name = x, Type = OperatorType.Comparison })); + } + var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance); + foreach (var method in methods) + { + if (method.ReturnType == typeof(Boolean) && !method.Name.StartsWith("get_") && !method.Name.StartsWith("set_") && !method.Name.StartsWith("_op")) + { + var paramaters = method.GetParameters(); + var op = new Operator() + { + Name = method.Name, + Type = OperatorType.ObjectMethod, + NumberOfInputs = paramaters.Length, + SimpleInputs = paramaters.All(x => Member.IsSimpleType(x.ParameterType)) + }; + if (noOverloads) + { + var existing = operators.FirstOrDefault(x => x.Name == op.Name && x.Type == op.Type); + if (existing == null) + operators.Add(op); + else if (existing.NumberOfInputs > op.NumberOfInputs) + { + operators[operators.IndexOf(existing)] = op; + } + } + else + operators.Add(op); + } + } + return operators; + } + } + + } + + [DataContract] + public class Rule + { + public Rule() + { + Inputs = Enumerable.Empty(); + } + + [DataMember] public string MemberName { get; set; } + [DataMember] public string Operator { get; set; } + [DataMember] public object TargetValue { get; set; } + [DataMember] public IList Rules { get; set; } + [DataMember] public IEnumerable Inputs { get; set; } + + + public static Rule operator |(Rule lhs, Rule rhs) + { + var rule = new Rule { Operator = "Or" }; + return MergeRulesInto(rule, lhs, rhs); + } + + public static Rule operator &(Rule lhs, Rule rhs) + { + var rule = new Rule { Operator = "AndAlso" }; + return MergeRulesInto(rule, lhs, rhs); + } + + private static Rule MergeRulesInto(Rule target, Rule lhs, Rule rhs) + { + target.Rules = new List(); + + if (lhs.Rules != null && lhs.Operator == target.Operator) // left is multiple + { + target.Rules.AddRange(lhs.Rules); + if (rhs.Rules != null && rhs.Operator == target.Operator) + target.Rules.AddRange(rhs.Rules); // left & right are multiple + else + target.Rules.Add(rhs); // left multi, right single + } + else if (rhs.Rules != null && rhs.Operator == target.Operator) + { + target.Rules.Add(lhs); // left single, right multi + target.Rules.AddRange(rhs.Rules); + } + else + { + target.Rules.Add(lhs); + target.Rules.Add(rhs); + } + + + return target; + } + + public static Rule Create(string member, mreOperator oper, object target) + { + return new Rule { MemberName = member, TargetValue = target, Operator = oper.ToString() }; + } + + public static Rule MethodOnChild(string member, string methodName, params object[] inputs) + { + return new Rule { MemberName = member, Inputs = inputs.ToList(), Operator = methodName }; + } + + public static Rule Method(string methodName, params object[] inputs) + { + return new Rule { Inputs = inputs.ToList(), Operator = methodName }; + } + + public static Rule Any(string member, Rule rule) + { + return new Rule { MemberName = member, Operator = "Any", Rules = new List { rule } }; + } + + public static Rule All(string member, Rule rule) + { + return new Rule { MemberName = member, Operator = "All", Rules = new List { rule } }; + } + + public static Rule IsInteger(string member) => new Rule() { MemberName = member, Operator = "IsInteger" }; + public static Rule IsFloat(string member) => new Rule() { MemberName = member, Operator = "IsSingle" }; + public static Rule IsDouble(string member) => new Rule() { MemberName = member, Operator = "IsDouble" }; + public static Rule IsSingle(string member) => new Rule() { MemberName = member, Operator = "IsSingle" }; + public static Rule IsDecimal(string member) => new Rule() { MemberName = member, Operator = "IsDecimal" }; public override string ToString() - { - if (TargetValue != null) - return $"{MemberName} {Operator} {TargetValue}"; - - if (Rules != null) - { - if (Rules.Count == 1) - return $"{MemberName} {Operator} ({Rules[0]})"; - else - return $"{MemberName} {Operator} (Rules)"; - } - - if (Inputs != null) - { - return $"{MemberName} {Operator} (Inputs)"; - } - - return $"{MemberName} {Operator}"; - } - } - - public class DataRule : Rule - { - public string Type { get; set; } - - public static DataRule Create(string member, mreOperator oper, T target) - { - return new DataRule - { - MemberName = member, - TargetValue = target, - Operator = oper.ToString(), - Type = typeof(T).FullName - }; - } - - public static DataRule Create(string member, mreOperator oper, string target) - { - return new DataRule - { - MemberName = member, - TargetValue = target, - Operator = oper.ToString(), - Type = typeof(T).FullName - }; - } - - - public static DataRule Create(string member, mreOperator oper, object target, Type memberType) - { - return new DataRule - { - MemberName = member, - TargetValue = target, - Operator = oper.ToString(), - Type = memberType.FullName - }; - } - } + { + if (TargetValue != null) + return $"{MemberName} {Operator} {TargetValue}"; + + if (Rules != null) + { + if (Rules.Count == 1) + return $"{MemberName} {Operator} ({Rules[0]})"; + else + return $"{MemberName} {Operator} (Rules)"; + } + + if (Inputs != null) + { + return $"{MemberName} {Operator} (Inputs)"; + } + + return $"{MemberName} {Operator}"; + } + } + + public class DataRule : Rule + { + public string Type { get; set; } + + public static DataRule Create(string member, mreOperator oper, T target) + { + return new DataRule + { + MemberName = member, + TargetValue = target, + Operator = oper.ToString(), + Type = typeof(T).FullName + }; + } + + public static DataRule Create(string member, mreOperator oper, string target) + { + return new DataRule + { + MemberName = member, + TargetValue = target, + Operator = oper.ToString(), + Type = typeof(T).FullName + }; + } + + + public static DataRule Create(string member, mreOperator oper, object target, Type memberType) + { + return new DataRule + { + MemberName = member, + TargetValue = target, + Operator = oper.ToString(), + Type = memberType.FullName + }; + } + } internal static class Placeholder { @@ -571,99 +796,117 @@ internal static class Placeholder // Nothing specific to MRE. Can be moved to a more common location public static class Extensions - { - public static void AddRange(this IList collection, IEnumerable newValues) - { - foreach (var item in newValues) - collection.Add(item); - } - } - - public class RulesException : ApplicationException - { - public RulesException() - { - } - - public RulesException(string message) : base(message) - { - } - - public RulesException(string message, Exception innerException) : base(message, innerException) - { - } - } - - - - // - // Summary: - // Describes the node types for the nodes of an expression tree. - public enum mreOperator - { - // - // Summary: - // An addition operation, such as a + b, without overflow checking, for numeric - // operands. - Add = 0, - // - // Summary: - // A bitwise or logical AND operation, such as (a & b) in C# and (a And b) in Visual - // Basic. - And = 2, - // - // Summary: - // A conditional AND operation that evaluates the second operand only if the first - // operand evaluates to true. It corresponds to (a && b) in C# and (a AndAlso b) - // in Visual Basic. - AndAlso = 3, - // - // Summary: - // A node that represents an equality comparison, such as (a == b) in C# or (a = - // b) in Visual Basic. - Equal = 13, - // - // Summary: - // A "greater than" comparison, such as (a > b). - GreaterThan = 15, - // - // Summary: - // A "greater than or equal to" comparison, such as (a >= b). - GreaterThanOrEqual = 16, - // - // Summary: - // A "less than" comparison, such as (a < b). - LessThan = 20, - // - // Summary: - // A "less than or equal to" comparison, such as (a <= b). - LessThanOrEqual = 21, - // - // Summary: - // An inequality comparison, such as (a != b) in C# or (a <> b) in Visual Basic. - NotEqual = 35, - // - // Summary: - // A bitwise or logical OR operation, such as (a | b) in C# or (a Or b) in Visual - // Basic. - Or = 36, - // - // Summary: - // A short-circuiting conditional OR operation, such as (a || b) in C# or (a OrElse - // b) in Visual Basic. - OrElse = 37, - - IsMatch - } - - - public class RuleValue - { - public T Value { get; set; } - public List Rules { get; set; } - } - - public class RuleValueString : RuleValue - { - } + { + public static void AddRange(this IList collection, IEnumerable newValues) + { + foreach (var item in newValues) + collection.Add(item); + } + } + + public class RulesException : ApplicationException + { + public RulesException() + { + } + + public RulesException(string message) : base(message) + { + } + + public RulesException(string message, Exception innerException) : base(message, innerException) + { + } + } + + + + // + // Summary: + // Describes the node types for the nodes of an expression tree. + public enum mreOperator + { + // + // Summary: + // An addition operation, such as a + b, without overflow checking, for numeric + // operands. + Add = 0, + // + // Summary: + // A bitwise or logical AND operation, such as (a & b) in C# and (a And b) in Visual + // Basic. + And = 2, + // + // Summary: + // A conditional AND operation that evaluates the second operand only if the first + // operand evaluates to true. It corresponds to (a && b) in C# and (a AndAlso b) + // in Visual Basic. + AndAlso = 3, + // + // Summary: + // A node that represents an equality comparison, such as (a == b) in C# or (a = + // b) in Visual Basic. + Equal = 13, + // + // Summary: + // A "greater than" comparison, such as (a > b). + GreaterThan = 15, + // + // Summary: + // A "greater than or equal to" comparison, such as (a >= b). + GreaterThanOrEqual = 16, + // + // Summary: + // A "less than" comparison, such as (a < b). + LessThan = 20, + // + // Summary: + // A "less than or equal to" comparison, such as (a <= b). + LessThanOrEqual = 21, + // + // Summary: + // An inequality comparison, such as (a != b) in C# or (a <> b) in Visual Basic. + NotEqual = 35, + // + // Summary: + // A bitwise or logical OR operation, such as (a | b) in C# or (a Or b) in Visual + // Basic. + Or = 36, + // + // Summary: + // A short-circuiting conditional OR operation, such as (a || b) in C# or (a OrElse + // b) in Visual Basic. + OrElse = 37, + /// + /// Checks that a string value matches a Regex expression + /// + IsMatch = 100, + /// + /// Checks that a value can be 'TryParsed' to an Int32 + /// + IsInteger = 101, + /// + /// Checks that a value can be 'TryParsed' to a Single + /// + IsSingle = 102, + /// + /// Checks that a value can be 'TryParsed' to a Double + /// + IsDouble = 103, + /// + /// Checks that a value can be 'TryParsed' to a Decimal + /// + IsDecimal = 104 + } + + + public class RuleValue + { + public T Value { get; set; } + public List Rules { get; set; } + } + + public class RuleValueString : RuleValue + { + } } diff --git a/Nuget/CreatePackage.bat b/Nuget/CreatePackage.bat index 1ffed3f..7be24b9 100644 --- a/Nuget/CreatePackage.bat +++ b/Nuget/CreatePackage.bat @@ -2,7 +2,7 @@ mkdir content mkdir content\MRE copy ..\MicroRuleEngine\MRE.cs content\MRE -NuGet pack MRE.nuspec -Exclude *.bat -Nuget push MRE.1.0.1.nupkg +nuget pack MRE.nuspec -Exclude *.bat +nuget push MRE.1.0.2.nupkg -Source https://api.nuget.org/v3/index.json pause \ No newline at end of file diff --git a/Nuget/MRE.nuspec b/Nuget/MRE.nuspec index 505eb4e..266d726 100644 --- a/Nuget/MRE.nuspec +++ b/Nuget/MRE.nuspec @@ -2,15 +2,15 @@ MRE - 1.0.1 - runxc1 + 1.0.2 + runxc1,jamescurran runxc1 https://github.com/runxc1/MicroRuleEngine false 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> - Initial Release. - Copyright 2012 - Micro RuleEngine business rules logic engine + Long awaited release adding a new api. + Copyright 2019 + dotnet RuleEngine Micro Rule-Engine Expression-Tree Rule Engine diff --git a/README.md b/README.md index 582e585..222a7c7 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,6 @@ -[![Build status](https://ci.appveyor.com/api/projects/status/uy7o0ch628v8qa8d?svg=true)](https://ci.appveyor.com/project/jamescurran/microruleengine) - MicroRuleEngine is a single file rule engine ============================================ -#### Fork Note: -On this fork, I've added a new API, since the original is rather unwieldy. With the new API, the Rule defined below in `ConditionalLogic()` can be written as: -```csharp -Rule rule = Rule.Create("Customer.LastName", mreOperator.Equal, "Doe") - & (Rule.Create("Customer.FirstName", mreOperator.Equal, "John") | Rule.Create("Customer.FirstName", mreOperator.Equal, "Jane")); -``` - NewApi.c in the UnitTest Project contains all the original unit tests re-written with the new API. - - I've also incorporated most additions from the various forks of this. Two notabky exceptions are that I have - not made MRE a static class (it seemed pointless, and prevents use of an IoC container), and I've left all the - source code in a single file (like the original author, I have a business need having it contained in a single file) - -Additionally, I've added unit tests/examples shows rules for testing integer & DateTime properties, and using -comparisons besides equality (These always worked; I just added demos of them). Also, examples of -serializing/deserializing a Rule as XML and JSON. (All these in the unit tests project) - - - the member property which are Arrays or List<>s (or can now accept a integer index: -```csharp -Rule rule = Rule.Create("Items[1].Cost", mreOperator.Equal, "5.25"); -``` - -- the new class `DataRule` allows defining rules which address ADO.NET DataSets: -```csharp -// (int)dataRow["Column2"] == 123 -DataRule.Create("Column2", mreOperator.Equal, "123") -``` - -- Add self-referential targets, indicated by the "*." at the beginning. -```csharp -Rule rule = Rule.Create("Items[1].Cost", mreOperator.Equal, "*.Items[0].Cost"); -``` - - Added tests for conversion of strings to numeric types: IsInt, IsFloat, IsDouble, IsDecimal - ```csharp -Rule rule = Rule.IsInteger("Column3"); - ``` - - (end fork note) - - 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 2 hundred lines. 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 @@ -78,7 +37,30 @@ Below is one of the tests. } ``` -You'll want to look at the Test project but just to give another snippet here is an example of Conditional logic in a test +What Kinds of Rules can I express +-------------------------------- +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 and add your own operator. Rules can also be ANDed or ORed together as shown below. + +```csharp + +Rule rule = Rule.Create("Customer.LastName", "Contains", "Do") + & (Rule.Create("Customer.FirstName", "StartsWith", "Jo")); + +``` + +You can reference member properties which are Arrays or List<> by their index +```csharp +Rule rule = Rule.Create("Items[1].Cost", mreOperator.GreaterThanOrEqual, "5.25"); +``` + +You can also compare an object to itself indicated by the "*." at the beginning of the TargetValue shown below +```csharp +Rule rule = Rule.Create("Items[1].Cost", mreOperator.Equal, "*.Items[0].Cost"); +``` + +There are a lot of examples in the Test cases but here is another snippet demonstrating nested OR logic ```csharp [TestMethod] @@ -111,6 +93,15 @@ You'll want to look at the Test project but just to give another snippet here is } ``` +If you need to run your comparison against an ADO.NET DataSet you can also do that as well. + +```csharp +Rule rule = Rule.Create("Items[1].Cost", mreOperator.Equal, "*.Items[0].Cost"); +``` + How do I store Rules? --------------------- The Rule Class is just a POCO so you can store your rules as serialized XML, JSON etc. + +#### 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 +