From 92a170e47ae1a032759904540d6406d866681ede Mon Sep 17 00:00:00 2001 From: Purunjay Bhal Date: Tue, 3 Jun 2025 04:03:54 +0530 Subject: [PATCH 1/3] adding support for automatic input-type registration --- src/RulesEngine/CustomTypeProvider.cs | 32 +++++++- src/RulesEngine/Models/ReSettings.cs | 2 +- src/RulesEngine/RulesEngine.cs | 34 +++++++- src/RulesEngine/RulesEngine.csproj | 2 +- .../CustomTypeProviderTests.cs | 81 +++++++++++++++---- .../RuleExpressionParserTests.cs | 66 ++++++++------- 6 files changed, 165 insertions(+), 52 deletions(-) diff --git a/src/RulesEngine/CustomTypeProvider.cs b/src/RulesEngine/CustomTypeProvider.cs index 1d5e3e76..58bbcbe7 100644 --- a/src/RulesEngine/CustomTypeProvider.cs +++ b/src/RulesEngine/CustomTypeProvider.cs @@ -3,7 +3,9 @@ using RulesEngine.HelperFunctions; using System; +using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Linq.Dynamic.Core; using System.Linq.Dynamic.Core.CustomTypeProviders; @@ -11,16 +13,40 @@ namespace RulesEngine { public class CustomTypeProvider : DefaultDynamicLinqCustomTypeProvider { - private HashSet _types; + private readonly HashSet _types; + public CustomTypeProvider(Type[] types) : base(ParsingConfig.Default) { - _types = new HashSet(types ?? new Type[] { }); + _types = new HashSet(types ?? Array.Empty()); + _types.Add(typeof(ExpressionUtils)); + + _types.Add(typeof(Enumerable)); + + var queue = new Queue(_types); + while (queue.Count > 0) + { + var t = queue.Dequeue(); + + var baseType = t.BaseType; + if (baseType != null && _types.Add(baseType)) + queue.Enqueue(baseType); + + foreach (var interfaceType in t.GetInterfaces()) + { + if (_types.Add(interfaceType)) + queue.Enqueue(interfaceType); + } + } + + _types.Add(typeof(IEnumerable)); } public override HashSet GetCustomTypes() { - return _types; + var all = new HashSet(base.GetCustomTypes()); + all.UnionWith(_types); + return all; } } } diff --git a/src/RulesEngine/Models/ReSettings.cs b/src/RulesEngine/Models/ReSettings.cs index 7071b131..247d637a 100644 --- a/src/RulesEngine/Models/ReSettings.cs +++ b/src/RulesEngine/Models/ReSettings.cs @@ -84,7 +84,7 @@ internal ReSettings(ReSettings reSettings) /// /// Whether to use FastExpressionCompiler for rule compilation /// - public bool UseFastExpressionCompiler { get; set; } = true; + public bool UseFastExpressionCompiler { get; set; } = false; /// /// Sets the mode for ParsingException to cascade to child elements and result in a expression parser /// Default: true diff --git a/src/RulesEngine/RulesEngine.cs b/src/RulesEngine/RulesEngine.cs index 627b7b43..f50a26f0 100644 --- a/src/RulesEngine/RulesEngine.cs +++ b/src/RulesEngine/RulesEngine.cs @@ -290,10 +290,16 @@ private bool RegisterRule(string workflowName, params RuleParameter[] ruleParams var dictFunc = new Dictionary>(); if (_reSettings.AutoRegisterInputType) { - //Disabling fast expression compiler if custom types are used - _reSettings.UseFastExpressionCompiler = (_reSettings.CustomTypes?.Length > 0) ? false : _reSettings.UseFastExpressionCompiler; - _reSettings.CustomTypes.Safe().Union(ruleParams.Select(c => c.Type)).ToArray(); + var collector = new HashSet(_reSettings.CustomTypes.Safe()); + + foreach (var rp in ruleParams) + { + CollectAllElementTypes(rp.Type, collector); + } + + _reSettings.CustomTypes = collector.ToArray(); } + // add separate compilation for global params var globalParamExp = new Lazy( @@ -338,7 +344,29 @@ private RuleFunc CompileRule(Rule rule, RuleExpressionType ruleE return _ruleCompiler.CompileRule(rule, ruleExpressionType, ruleParams, scopedParams); } + private static void CollectAllElementTypes(Type t, ISet collector) + { + if (t == null || collector.Contains(t)) + return; + + collector.Add(t); + + if (t.IsGenericType) + { + foreach (var ga in t.GetGenericArguments()) + CollectAllElementTypes(ga, collector); + } + + if (t.IsArray) + { + CollectAllElementTypes(t.GetElementType(), collector); + } + if (Nullable.GetUnderlyingType(t) is Type underly && !collector.Contains(underly)) + { + CollectAllElementTypes(underly, collector); + } + } /// /// This will execute the compiled rules diff --git a/src/RulesEngine/RulesEngine.csproj b/src/RulesEngine/RulesEngine.csproj index d777f0e0..4c8c5cda 100644 --- a/src/RulesEngine/RulesEngine.csproj +++ b/src/RulesEngine/RulesEngine.csproj @@ -3,7 +3,7 @@ net6.0;net8.0;net9.0;netstandard2.0 13.0 - 5.0.6 + 5.0.7 Copyright (c) Microsoft Corporation. LICENSE https://github.com/microsoft/RulesEngine diff --git a/test/RulesEngine.UnitTest/CustomTypeProviderTests.cs b/test/RulesEngine.UnitTest/CustomTypeProviderTests.cs index 4564791f..043e8d4e 100644 --- a/test/RulesEngine.UnitTest/CustomTypeProviderTests.cs +++ b/test/RulesEngine.UnitTest/CustomTypeProviderTests.cs @@ -3,7 +3,9 @@ using Moq; using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Xunit; namespace RulesEngine.UnitTest @@ -12,33 +14,84 @@ namespace RulesEngine.UnitTest [ExcludeFromCodeCoverage] public class CustomTypeProviderTests : IDisposable { - private readonly MockRepository _mockRepository; - public CustomTypeProviderTests() + public void Dispose() { - _mockRepository = new MockRepository(MockBehavior.Strict); } - public void Dispose() + private CustomTypeProvider CreateProvider(params Type[] customTypes) { - _mockRepository.VerifyAll(); + return new CustomTypeProvider(customTypes); } - private CustomTypeProvider CreateProvider() + [Fact] + public void GetCustomTypes_DefaultProvider_IncludesEnumerableAndObject() { - return new CustomTypeProvider(null); + var provider = CreateProvider(); + var allTypes = provider.GetCustomTypes(); + Assert.NotEmpty(allTypes); + Assert.Contains(typeof(System.Linq.Enumerable), allTypes); + Assert.Contains(typeof(object), allTypes); } [Fact] - public void GetCustomTypes_StateUnderTest_ExpectedBehavior() + public void GetCustomTypes_WithListOfGuid_ContainsIEnumerableOfGuid() { - // Arrange - var unitUnderTest = CreateProvider(); + var initial = new[] { typeof(List) }; + var provider = CreateProvider(initial); + var allTypes = provider.GetCustomTypes(); + Assert.Contains(typeof(IEnumerable), allTypes); + Assert.Contains(typeof(List), allTypes); + Assert.Contains(typeof(System.Linq.Enumerable), allTypes); + Assert.Contains(typeof(object), allTypes); + } - // Act - var result = unitUnderTest.GetCustomTypes(); + [Fact] + public void GetCustomTypes_ListOfListString_ContainsIEnumerableOfListString() + { + var nestedListType = typeof(List>); + var provider = CreateProvider(nestedListType); + var allTypes = provider.GetCustomTypes(); + Assert.Contains(typeof(IEnumerable>), allTypes); + Assert.Contains(nestedListType, allTypes); + Assert.Contains(typeof(System.Linq.Enumerable), allTypes); + Assert.Contains(typeof(object), allTypes); + } - // Assert - Assert.NotEmpty(result); + [Fact] + public void GetCustomTypes_ArrayOfStringArrays_ContainsIEnumerableOfStringArray() + { + var arrayType = typeof(string[][]); + var provider = CreateProvider(arrayType); + var allTypes = provider.GetCustomTypes(); + Assert.Contains(typeof(IEnumerable), allTypes); + Assert.Contains(arrayType, allTypes); + Assert.Contains(typeof(System.Linq.Enumerable), allTypes); + Assert.Contains(typeof(object), allTypes); + } + + [Fact] + public void GetCustomTypes_NullableIntArray_ContainsIEnumerableOfNullableInt() + { + var nullableInt = typeof(int?); + var arrayType = typeof(int?[]); + var provider = CreateProvider(arrayType); + var allTypes = provider.GetCustomTypes(); + Assert.Contains(typeof(IEnumerable), allTypes); + Assert.Contains(arrayType, allTypes); + Assert.Contains(typeof(System.Linq.Enumerable), allTypes); + Assert.Contains(typeof(object), allTypes); + } + + [Fact] + public void GetCustomTypes_MultipleTypes_NoDuplicates() + { + var repeatedType = typeof(List); + var provider = CreateProvider(repeatedType, repeatedType); + var allTypes = provider.GetCustomTypes(); + var matches = allTypes.Where(t => t == repeatedType).ToList(); + Assert.Single(matches); + var interfaceMatches = allTypes.Where(t => t == typeof(IEnumerable)).ToList(); + Assert.Single(interfaceMatches); } } } diff --git a/test/RulesEngine.UnitTest/RuleExpressionParserTests/RuleExpressionParserTests.cs b/test/RulesEngine.UnitTest/RuleExpressionParserTests/RuleExpressionParserTests.cs index c69b5be8..aa0ffb4e 100644 --- a/test/RulesEngine.UnitTest/RuleExpressionParserTests/RuleExpressionParserTests.cs +++ b/test/RulesEngine.UnitTest/RuleExpressionParserTests/RuleExpressionParserTests.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json.Linq; using RulesEngine.ExpressionBuilders; +using RulesEngine.Models; using System.Diagnostics.CodeAnalysis; using Xunit; @@ -20,38 +21,43 @@ public RuleExpressionParserTests() { [Fact] public void TestExpressionWithJObject() { - var ruleParser = new RuleExpressionParser(new Models.ReSettings()); - - var inputStr = @"{ + var settings = new ReSettings { + CustomTypes = new[] + { + typeof(JObject), + typeof(JToken), + typeof(JArray) + } + }; + var parser = new RuleExpressionParser(settings); + + var json = @"{ ""list"": [ - { ""item1"": ""hello"", - ""item3"": 1 - }, - { - ""item2"": ""world"" - } - ] + { ""item1"": ""hello"", ""item3"": 1 }, + { ""item2"": ""world"" } + ] }"; - - - var input = JObject.Parse(inputStr); - - - var value = ruleParser.Evaluate("input.list[0].item3 == 1", new[] { new Models.RuleParameter("input", input) }); - - Assert.Equal(true, - value); - - - var value2 = ruleParser.Evaluate("input.list[1].item2 == \"world\"", new[] { new Models.RuleParameter("input", input) }); - - Assert.Equal(true, - value2); - - - var value3= ruleParser.Evaluate("string.Concat(input.list[0].item1,input.list[1].item2)", new[] { new Models.RuleParameter("input", input) }); - - Assert.Equal("helloworld", value3); + var input = JObject.Parse(json); + + var result1 = parser.Evaluate( + "Convert.ToInt32(input[\"list\"][0][\"item3\"]) == 1", + new[] { new RuleParameter("input", input) } + ); + Assert.True((bool)result1); + + var result2 = parser.Evaluate( + "Convert.ToString(input[\"list\"][1][\"item2\"]) == \"world\"", + new[] { new RuleParameter("input", input) } + ); + Assert.True((bool)result2); + + var result3 = parser.Evaluate( + "string.Concat(" + + "Convert.ToString(input[\"list\"][0][\"item1\"]), " + + "Convert.ToString(input[\"list\"][1][\"item2\"]))", + new[] { new RuleParameter("input", input) } + ); + Assert.Equal("helloworld", result3); } [Theory] From d20bb3c3591aaf7e466b67ea6f389384316d70ed Mon Sep 17 00:00:00 2001 From: Purunjay Bhal Date: Tue, 3 Jun 2025 04:08:17 +0530 Subject: [PATCH 2/3] updating nuget version --- src/RulesEngine/RulesEngine.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RulesEngine/RulesEngine.csproj b/src/RulesEngine/RulesEngine.csproj index 4c8c5cda..f723b384 100644 --- a/src/RulesEngine/RulesEngine.csproj +++ b/src/RulesEngine/RulesEngine.csproj @@ -3,7 +3,7 @@ net6.0;net8.0;net9.0;netstandard2.0 13.0 - 5.0.7 + 6.0.0 Copyright (c) Microsoft Corporation. LICENSE https://github.com/microsoft/RulesEngine From d5a49d4ec1c1c8242fa86d1dcdbea0dbc7a8d19e Mon Sep 17 00:00:00 2001 From: Purunjay Bhal Date: Tue, 3 Jun 2025 04:16:23 +0530 Subject: [PATCH 3/3] updating code coverage --- .github/workflows/dotnetcore-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnetcore-build.yml b/.github/workflows/dotnetcore-build.yml index c59946ce..2e775a36 100644 --- a/.github/workflows/dotnetcore-build.yml +++ b/.github/workflows/dotnetcore-build.yml @@ -34,7 +34,7 @@ jobs: - name: Check Coverage shell: pwsh - run: ./scripts/check-coverage.ps1 -reportPath coveragereport/Cobertura.xml -threshold 92 + run: ./scripts/check-coverage.ps1 -reportPath coveragereport/Cobertura.xml -threshold 94 - name: Coveralls GitHub Action uses: coverallsapp/github-action@v2.3.6