Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/dotnetcore-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 29 additions & 3 deletions src/RulesEngine/CustomTypeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,50 @@

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;

namespace RulesEngine
{
public class CustomTypeProvider : DefaultDynamicLinqCustomTypeProvider
{
private HashSet<Type> _types;
private readonly HashSet<Type> _types;

public CustomTypeProvider(Type[] types) : base(ParsingConfig.Default)
{
_types = new HashSet<Type>(types ?? new Type[] { });
_types = new HashSet<Type>(types ?? Array.Empty<Type>());

_types.Add(typeof(ExpressionUtils));

_types.Add(typeof(Enumerable));

var queue = new Queue<Type>(_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<Type> GetCustomTypes()
{
return _types;
var all = new HashSet<Type>(base.GetCustomTypes());
all.UnionWith(_types);
return all;
}
}
}
2 changes: 1 addition & 1 deletion src/RulesEngine/Models/ReSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ internal ReSettings(ReSettings reSettings)
/// <summary>
/// Whether to use FastExpressionCompiler for rule compilation
/// </summary>
public bool UseFastExpressionCompiler { get; set; } = true;
public bool UseFastExpressionCompiler { get; set; } = false;
/// <summary>
/// Sets the mode for ParsingException to cascade to child elements and result in a expression parser
/// Default: true
Expand Down
34 changes: 31 additions & 3 deletions src/RulesEngine/RulesEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -290,10 +290,16 @@ private bool RegisterRule(string workflowName, params RuleParameter[] ruleParams
var dictFunc = new Dictionary<string, RuleFunc<RuleResultTree>>();
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<Type>(_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<RuleExpressionParameter[]>(
Expand Down Expand Up @@ -338,7 +344,29 @@ private RuleFunc<RuleResultTree> CompileRule(Rule rule, RuleExpressionType ruleE
return _ruleCompiler.CompileRule(rule, ruleExpressionType, ruleParams, scopedParams);
}

private static void CollectAllElementTypes(Type t, ISet<Type> 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);
}
}

/// <summary>
/// This will execute the compiled rules
Expand Down
2 changes: 1 addition & 1 deletion src/RulesEngine/RulesEngine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0;net9.0;netstandard2.0</TargetFrameworks>
<LangVersion>13.0</LangVersion>
<Version>5.0.6</Version>
<Version>6.0.0</Version>
<Copyright>Copyright (c) Microsoft Corporation.</Copyright>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://github.com/microsoft/RulesEngine</PackageProjectUrl>
Expand Down
81 changes: 67 additions & 14 deletions test/RulesEngine.UnitTest/CustomTypeProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

using Moq;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Xunit;

namespace RulesEngine.UnitTest
Expand All @@ -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<Guid>) };
var provider = CreateProvider(initial);
var allTypes = provider.GetCustomTypes();
Assert.Contains(typeof(IEnumerable<Guid>), allTypes);
Assert.Contains(typeof(List<Guid>), 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<List<string>>);
var provider = CreateProvider(nestedListType);
var allTypes = provider.GetCustomTypes();
Assert.Contains(typeof(IEnumerable<List<string>>), 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<string[]>), 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<int?>), 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<string>);
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<string>)).ToList();
Assert.Single(interfaceMatches);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Newtonsoft.Json.Linq;
using RulesEngine.ExpressionBuilders;
using RulesEngine.Models;
using System.Diagnostics.CodeAnalysis;
using Xunit;

Expand All @@ -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<object>("input.list[0].item3 == 1", new[] { new Models.RuleParameter("input", input) });

Assert.Equal(true,
value);


var value2 = ruleParser.Evaluate<object>("input.list[1].item2 == \"world\"", new[] { new Models.RuleParameter("input", input) });

Assert.Equal(true,
value2);


var value3= ruleParser.Evaluate<object>("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<object>(
"Convert.ToInt32(input[\"list\"][0][\"item3\"]) == 1",
new[] { new RuleParameter("input", input) }
);
Assert.True((bool)result1);

var result2 = parser.Evaluate<object>(
"Convert.ToString(input[\"list\"][1][\"item2\"]) == \"world\"",
new[] { new RuleParameter("input", input) }
);
Assert.True((bool)result2);

var result3 = parser.Evaluate<object>(
"string.Concat(" +
"Convert.ToString(input[\"list\"][0][\"item1\"]), " +
"Convert.ToString(input[\"list\"][1][\"item2\"]))",
new[] { new RuleParameter("input", input) }
);
Assert.Equal("helloworld", result3);
}

[Theory]
Expand Down
Loading