Skip to content

Commit 71d59dc

Browse files
authored
[Bug Fix] Handling of Automatic Type Registration (#675)
* adding support for automatic input-type registration * updating nuget version * updating code coverage
1 parent b1264a2 commit 71d59dc

File tree

7 files changed

+166
-53
lines changed

7 files changed

+166
-53
lines changed

.github/workflows/dotnetcore-build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434

3535
- name: Check Coverage
3636
shell: pwsh
37-
run: ./scripts/check-coverage.ps1 -reportPath coveragereport/Cobertura.xml -threshold 92
37+
run: ./scripts/check-coverage.ps1 -reportPath coveragereport/Cobertura.xml -threshold 94
3838

3939
- name: Coveralls GitHub Action
4040
uses: coverallsapp/[email protected]

src/RulesEngine/CustomTypeProvider.cs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,50 @@
33

44
using RulesEngine.HelperFunctions;
55
using System;
6+
using System.Collections;
67
using System.Collections.Generic;
8+
using System.Linq;
79
using System.Linq.Dynamic.Core;
810
using System.Linq.Dynamic.Core.CustomTypeProviders;
911

1012
namespace RulesEngine
1113
{
1214
public class CustomTypeProvider : DefaultDynamicLinqCustomTypeProvider
1315
{
14-
private HashSet<Type> _types;
16+
private readonly HashSet<Type> _types;
17+
1518
public CustomTypeProvider(Type[] types) : base(ParsingConfig.Default)
1619
{
17-
_types = new HashSet<Type>(types ?? new Type[] { });
20+
_types = new HashSet<Type>(types ?? Array.Empty<Type>());
21+
1822
_types.Add(typeof(ExpressionUtils));
23+
24+
_types.Add(typeof(Enumerable));
25+
26+
var queue = new Queue<Type>(_types);
27+
while (queue.Count > 0)
28+
{
29+
var t = queue.Dequeue();
30+
31+
var baseType = t.BaseType;
32+
if (baseType != null && _types.Add(baseType))
33+
queue.Enqueue(baseType);
34+
35+
foreach (var interfaceType in t.GetInterfaces())
36+
{
37+
if (_types.Add(interfaceType))
38+
queue.Enqueue(interfaceType);
39+
}
40+
}
41+
42+
_types.Add(typeof(IEnumerable));
1943
}
2044

2145
public override HashSet<Type> GetCustomTypes()
2246
{
23-
return _types;
47+
var all = new HashSet<Type>(base.GetCustomTypes());
48+
all.UnionWith(_types);
49+
return all;
2450
}
2551
}
2652
}

src/RulesEngine/Models/ReSettings.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ internal ReSettings(ReSettings reSettings)
8484
/// <summary>
8585
/// Whether to use FastExpressionCompiler for rule compilation
8686
/// </summary>
87-
public bool UseFastExpressionCompiler { get; set; } = true;
87+
public bool UseFastExpressionCompiler { get; set; } = false;
8888
/// <summary>
8989
/// Sets the mode for ParsingException to cascade to child elements and result in a expression parser
9090
/// Default: true

src/RulesEngine/RulesEngine.cs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -290,10 +290,16 @@ private bool RegisterRule(string workflowName, params RuleParameter[] ruleParams
290290
var dictFunc = new Dictionary<string, RuleFunc<RuleResultTree>>();
291291
if (_reSettings.AutoRegisterInputType)
292292
{
293-
//Disabling fast expression compiler if custom types are used
294-
_reSettings.UseFastExpressionCompiler = (_reSettings.CustomTypes?.Length > 0) ? false : _reSettings.UseFastExpressionCompiler;
295-
_reSettings.CustomTypes.Safe().Union(ruleParams.Select(c => c.Type)).ToArray();
293+
var collector = new HashSet<Type>(_reSettings.CustomTypes.Safe());
294+
295+
foreach (var rp in ruleParams)
296+
{
297+
CollectAllElementTypes(rp.Type, collector);
298+
}
299+
300+
_reSettings.CustomTypes = collector.ToArray();
296301
}
302+
297303
// add separate compilation for global params
298304

299305
var globalParamExp = new Lazy<RuleExpressionParameter[]>(
@@ -338,7 +344,29 @@ private RuleFunc<RuleResultTree> CompileRule(Rule rule, RuleExpressionType ruleE
338344
return _ruleCompiler.CompileRule(rule, ruleExpressionType, ruleParams, scopedParams);
339345
}
340346

347+
private static void CollectAllElementTypes(Type t, ISet<Type> collector)
348+
{
349+
if (t == null || collector.Contains(t))
350+
return;
351+
352+
collector.Add(t);
353+
354+
if (t.IsGenericType)
355+
{
356+
foreach (var ga in t.GetGenericArguments())
357+
CollectAllElementTypes(ga, collector);
358+
}
359+
360+
if (t.IsArray)
361+
{
362+
CollectAllElementTypes(t.GetElementType(), collector);
363+
}
341364

365+
if (Nullable.GetUnderlyingType(t) is Type underly && !collector.Contains(underly))
366+
{
367+
CollectAllElementTypes(underly, collector);
368+
}
369+
}
342370

343371
/// <summary>
344372
/// This will execute the compiled rules

src/RulesEngine/RulesEngine.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<PropertyGroup>
44
<TargetFrameworks>net6.0;net8.0;net9.0;netstandard2.0</TargetFrameworks>
55
<LangVersion>13.0</LangVersion>
6-
<Version>5.0.6</Version>
6+
<Version>6.0.0</Version>
77
<Copyright>Copyright (c) Microsoft Corporation.</Copyright>
88
<PackageLicenseFile>LICENSE</PackageLicenseFile>
99
<PackageProjectUrl>https://github.com/microsoft/RulesEngine</PackageProjectUrl>

test/RulesEngine.UnitTest/CustomTypeProviderTests.cs

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
using Moq;
55
using System;
6+
using System.Collections.Generic;
67
using System.Diagnostics.CodeAnalysis;
8+
using System.Linq;
79
using Xunit;
810

911
namespace RulesEngine.UnitTest
@@ -12,33 +14,84 @@ namespace RulesEngine.UnitTest
1214
[ExcludeFromCodeCoverage]
1315
public class CustomTypeProviderTests : IDisposable
1416
{
15-
private readonly MockRepository _mockRepository;
16-
public CustomTypeProviderTests()
17+
public void Dispose()
1718
{
18-
_mockRepository = new MockRepository(MockBehavior.Strict);
1919
}
2020

21-
public void Dispose()
21+
private CustomTypeProvider CreateProvider(params Type[] customTypes)
2222
{
23-
_mockRepository.VerifyAll();
23+
return new CustomTypeProvider(customTypes);
2424
}
2525

26-
private CustomTypeProvider CreateProvider()
26+
[Fact]
27+
public void GetCustomTypes_DefaultProvider_IncludesEnumerableAndObject()
2728
{
28-
return new CustomTypeProvider(null);
29+
var provider = CreateProvider();
30+
var allTypes = provider.GetCustomTypes();
31+
Assert.NotEmpty(allTypes);
32+
Assert.Contains(typeof(System.Linq.Enumerable), allTypes);
33+
Assert.Contains(typeof(object), allTypes);
2934
}
3035

3136
[Fact]
32-
public void GetCustomTypes_StateUnderTest_ExpectedBehavior()
37+
public void GetCustomTypes_WithListOfGuid_ContainsIEnumerableOfGuid()
3338
{
34-
// Arrange
35-
var unitUnderTest = CreateProvider();
39+
var initial = new[] { typeof(List<Guid>) };
40+
var provider = CreateProvider(initial);
41+
var allTypes = provider.GetCustomTypes();
42+
Assert.Contains(typeof(IEnumerable<Guid>), allTypes);
43+
Assert.Contains(typeof(List<Guid>), allTypes);
44+
Assert.Contains(typeof(System.Linq.Enumerable), allTypes);
45+
Assert.Contains(typeof(object), allTypes);
46+
}
3647

37-
// Act
38-
var result = unitUnderTest.GetCustomTypes();
48+
[Fact]
49+
public void GetCustomTypes_ListOfListString_ContainsIEnumerableOfListString()
50+
{
51+
var nestedListType = typeof(List<List<string>>);
52+
var provider = CreateProvider(nestedListType);
53+
var allTypes = provider.GetCustomTypes();
54+
Assert.Contains(typeof(IEnumerable<List<string>>), allTypes);
55+
Assert.Contains(nestedListType, allTypes);
56+
Assert.Contains(typeof(System.Linq.Enumerable), allTypes);
57+
Assert.Contains(typeof(object), allTypes);
58+
}
3959

40-
// Assert
41-
Assert.NotEmpty(result);
60+
[Fact]
61+
public void GetCustomTypes_ArrayOfStringArrays_ContainsIEnumerableOfStringArray()
62+
{
63+
var arrayType = typeof(string[][]);
64+
var provider = CreateProvider(arrayType);
65+
var allTypes = provider.GetCustomTypes();
66+
Assert.Contains(typeof(IEnumerable<string[]>), allTypes);
67+
Assert.Contains(arrayType, allTypes);
68+
Assert.Contains(typeof(System.Linq.Enumerable), allTypes);
69+
Assert.Contains(typeof(object), allTypes);
70+
}
71+
72+
[Fact]
73+
public void GetCustomTypes_NullableIntArray_ContainsIEnumerableOfNullableInt()
74+
{
75+
var nullableInt = typeof(int?);
76+
var arrayType = typeof(int?[]);
77+
var provider = CreateProvider(arrayType);
78+
var allTypes = provider.GetCustomTypes();
79+
Assert.Contains(typeof(IEnumerable<int?>), allTypes);
80+
Assert.Contains(arrayType, allTypes);
81+
Assert.Contains(typeof(System.Linq.Enumerable), allTypes);
82+
Assert.Contains(typeof(object), allTypes);
83+
}
84+
85+
[Fact]
86+
public void GetCustomTypes_MultipleTypes_NoDuplicates()
87+
{
88+
var repeatedType = typeof(List<string>);
89+
var provider = CreateProvider(repeatedType, repeatedType);
90+
var allTypes = provider.GetCustomTypes();
91+
var matches = allTypes.Where(t => t == repeatedType).ToList();
92+
Assert.Single(matches);
93+
var interfaceMatches = allTypes.Where(t => t == typeof(IEnumerable<string>)).ToList();
94+
Assert.Single(interfaceMatches);
4295
}
4396
}
4497
}

test/RulesEngine.UnitTest/RuleExpressionParserTests/RuleExpressionParserTests.cs

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using Newtonsoft.Json.Linq;
55
using RulesEngine.ExpressionBuilders;
6+
using RulesEngine.Models;
67
using System.Diagnostics.CodeAnalysis;
78
using Xunit;
89

@@ -20,38 +21,43 @@ public RuleExpressionParserTests() {
2021
[Fact]
2122
public void TestExpressionWithJObject()
2223
{
23-
var ruleParser = new RuleExpressionParser(new Models.ReSettings());
24-
25-
var inputStr = @"{
24+
var settings = new ReSettings {
25+
CustomTypes = new[]
26+
{
27+
typeof(JObject),
28+
typeof(JToken),
29+
typeof(JArray)
30+
}
31+
};
32+
var parser = new RuleExpressionParser(settings);
33+
34+
var json = @"{
2635
""list"": [
27-
{ ""item1"": ""hello"",
28-
""item3"": 1
29-
},
30-
{
31-
""item2"": ""world""
32-
}
33-
]
36+
{ ""item1"": ""hello"", ""item3"": 1 },
37+
{ ""item2"": ""world"" }
38+
]
3439
}";
35-
36-
37-
var input = JObject.Parse(inputStr);
38-
39-
40-
var value = ruleParser.Evaluate<object>("input.list[0].item3 == 1", new[] { new Models.RuleParameter("input", input) });
41-
42-
Assert.Equal(true,
43-
value);
44-
45-
46-
var value2 = ruleParser.Evaluate<object>("input.list[1].item2 == \"world\"", new[] { new Models.RuleParameter("input", input) });
47-
48-
Assert.Equal(true,
49-
value2);
50-
51-
52-
var value3= ruleParser.Evaluate<object>("string.Concat(input.list[0].item1,input.list[1].item2)", new[] { new Models.RuleParameter("input", input) });
53-
54-
Assert.Equal("helloworld", value3);
40+
var input = JObject.Parse(json);
41+
42+
var result1 = parser.Evaluate<object>(
43+
"Convert.ToInt32(input[\"list\"][0][\"item3\"]) == 1",
44+
new[] { new RuleParameter("input", input) }
45+
);
46+
Assert.True((bool)result1);
47+
48+
var result2 = parser.Evaluate<object>(
49+
"Convert.ToString(input[\"list\"][1][\"item2\"]) == \"world\"",
50+
new[] { new RuleParameter("input", input) }
51+
);
52+
Assert.True((bool)result2);
53+
54+
var result3 = parser.Evaluate<object>(
55+
"string.Concat(" +
56+
"Convert.ToString(input[\"list\"][0][\"item1\"]), " +
57+
"Convert.ToString(input[\"list\"][1][\"item2\"]))",
58+
new[] { new RuleParameter("input", input) }
59+
);
60+
Assert.Equal("helloworld", result3);
5561
}
5662

5763
[Theory]

0 commit comments

Comments
 (0)