diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 6cfe968b..af5df500 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -44,6 +44,7 @@ jobs: 6.0.x 8.0.x 9.0.x + 10.0.x # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/dotnetcore-build.yml b/.github/workflows/dotnetcore-build.yml index 2e775a36..b35626b2 100644 --- a/.github/workflows/dotnetcore-build.yml +++ b/.github/workflows/dotnetcore-build.yml @@ -18,6 +18,7 @@ jobs: 6.0.x 8.0.x 9.0.x + 10.0.x - name: Install dependencies run: dotnet restore RulesEngine.sln diff --git a/benchmark/RulesEngineBenchmark/RulesEngineBenchmark.csproj b/benchmark/RulesEngineBenchmark/RulesEngineBenchmark.csproj index 0eb2c7bf..fc9e552b 100644 --- a/benchmark/RulesEngineBenchmark/RulesEngineBenchmark.csproj +++ b/benchmark/RulesEngineBenchmark/RulesEngineBenchmark.csproj @@ -2,7 +2,7 @@ Exe - net6.0;net8.0;net9.0 + net6.0;net8.0;net9.0;net10.0 diff --git a/demo/DemoApp.EFDataExample/DemoApp.EFDataExample.csproj b/demo/DemoApp.EFDataExample/DemoApp.EFDataExample.csproj index 40577574..03ce0f47 100644 --- a/demo/DemoApp.EFDataExample/DemoApp.EFDataExample.csproj +++ b/demo/DemoApp.EFDataExample/DemoApp.EFDataExample.csproj @@ -1,7 +1,7 @@ - net8.0;net9.0 + net8.0;net9.0;net10.0 DemoApp.EFDataExample DemoApp.EFDataExample diff --git a/demo/DemoApp/DemoApp.csproj b/demo/DemoApp/DemoApp.csproj index 83f0d8a7..bb37d79a 100644 --- a/demo/DemoApp/DemoApp.csproj +++ b/demo/DemoApp/DemoApp.csproj @@ -2,7 +2,7 @@ Exe - net8.0;net9.0 + net8.0;net9.0;net10.0 DemoApp.Program diff --git a/global.json b/global.json index a1481352..010597ab 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "9.0.301", + "version": "10.0.100", "rollForward": "latestFeature", - "allowPrerelease": false + "allowPrerelease": true } } \ No newline at end of file diff --git a/src/RulesEngine/HelperFunctions/Utils.cs b/src/RulesEngine/HelperFunctions/Utils.cs index 05df93dd..98c0953a 100644 --- a/src/RulesEngine/HelperFunctions/Utils.cs +++ b/src/RulesEngine/HelperFunctions/Utils.cs @@ -19,6 +19,11 @@ public static object GetTypedObject(dynamic input) Type type = CreateAbstractClassType(input); return CreateObject(type, input); } + else if (input is IDictionary dict) + { + Type type = CreateAbstractClassTypeFromDictionary(dict); + return CreateObjectFromDictionary(type, dict); + } else { return input; @@ -129,6 +134,89 @@ private static IList ToList(this IEnumerable self, Type innerType) var genericMethod = methodInfo.MakeGenericMethod(innerType); return genericMethod.Invoke(null, new[] { self }) as IList; } + + private static Type CreateAbstractClassTypeFromDictionary(IDictionary dictionary) + { + List props = []; + + foreach (var kvp in dictionary) + { + Type valueType; + if (kvp.Value is ExpandoObject) + { + valueType = CreateAbstractClassType(kvp.Value); + } + else if (kvp.Value is IDictionary nestedDict) + { + valueType = CreateAbstractClassTypeFromDictionary(nestedDict); + } + else if (kvp.Value is IList list) + { + if (list.Count == 0) + { + valueType = typeof(List); + } + else + { + var internalType = list[0] is IDictionary innerDict + ? CreateAbstractClassTypeFromDictionary(innerDict) + : (list[0] is ExpandoObject ? CreateAbstractClassType(list[0]) : list[0]?.GetType() ?? typeof(object)); + valueType = new List().Cast(internalType).ToList(internalType).GetType(); + } + } + else + { + valueType = kvp.Value?.GetType() ?? typeof(object); + } + props.Add(new DynamicProperty(kvp.Key, valueType)); + } + + return DynamicClassFactory.CreateType(props); + } + + private static object CreateObjectFromDictionary(Type type, IDictionary dictionary) + { + var obj = Activator.CreateInstance(type); + var typeProps = type.GetProperties().ToDictionary(c => c.Name); + + foreach (var kvp in dictionary) + { + if (typeProps.ContainsKey(kvp.Key) && + kvp.Value != null && (kvp.Value.GetType().Name != "DBNull" || kvp.Value != DBNull.Value)) + { + object val; + var propInfo = typeProps[kvp.Key]; + if (kvp.Value is ExpandoObject) + { + val = CreateObject(propInfo.PropertyType, kvp.Value); + } + else if (kvp.Value is IDictionary nestedDict) + { + val = CreateObjectFromDictionary(propInfo.PropertyType, nestedDict); + } + else if (kvp.Value is IList temp) + { + var internalType = propInfo.PropertyType.GenericTypeArguments.FirstOrDefault() ?? typeof(object); + var newList = new List().Cast(internalType).ToList(internalType); + foreach (var t in temp) + { + var child = t is IDictionary d + ? CreateObjectFromDictionary(internalType, d) + : (t is ExpandoObject ? CreateObject(internalType, t) : t); + newList.Add(child); + } + val = newList; + } + else + { + val = kvp.Value; + } + propInfo.SetValue(obj, val, null); + } + } + + return obj; + } } diff --git a/src/RulesEngine/RulesEngine.csproj b/src/RulesEngine/RulesEngine.csproj index e8ce3769..f146052e 100644 --- a/src/RulesEngine/RulesEngine.csproj +++ b/src/RulesEngine/RulesEngine.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0;net9.0;netstandard2.0 + net6.0;net8.0;net9.0;net10.0;netstandard2.0;netstandard2.1 13.0 6.0.0 Copyright (c) Microsoft Corporation. @@ -41,7 +41,7 @@ - + @@ -53,7 +53,7 @@ - + diff --git a/test/RulesEngine.UnitTest/RuleExpressionParserTests/RuleExpressionParserTests.cs b/test/RulesEngine.UnitTest/RuleExpressionParserTests/RuleExpressionParserTests.cs index aa0ffb4e..0be1fe93 100644 --- a/test/RulesEngine.UnitTest/RuleExpressionParserTests/RuleExpressionParserTests.cs +++ b/test/RulesEngine.UnitTest/RuleExpressionParserTests/RuleExpressionParserTests.cs @@ -69,6 +69,42 @@ public void TestExpressionWithDifferentCompilerSettings(bool fastExpressionEnabl var result = ruleParser.Evaluate("d1 < 20", new[] { Models.RuleParameter.Create("d1", d1) }); Assert.False(result); } + + [Fact] + public void TestExpressionWithDictionaryParameter() + { + var parser = new RuleExpressionParser(new ReSettings()); + + var payload = new System.Collections.Generic.Dictionary + { + { "Formule", "Essentielle" } + }; + + var ruleParameters = new[] { RuleParameter.Create("_", payload) }; + + var resultNotEqual = parser.Evaluate("Formule != \"Essentielle\"", ruleParameters); + Assert.False(resultNotEqual); + + var resultEqual = parser.Evaluate("Formule == \"Essentielle\"", ruleParameters); + Assert.True(resultEqual); + } + + [Fact] + public void TestExpressionWithDictionaryParameter_MultipleKeys() + { + var parser = new RuleExpressionParser(new ReSettings()); + + var payload = new System.Collections.Generic.Dictionary + { + { "Name", "John" }, + { "Age", 30 } + }; + + var ruleParameters = new[] { RuleParameter.Create("input", payload) }; + + var result = parser.Evaluate("Name == \"John\" && Age == 30", ruleParameters); + Assert.True(result); + } } diff --git a/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj b/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj index 281ea8a6..f33bcd57 100644 --- a/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj +++ b/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj @@ -1,6 +1,6 @@ - net6.0;net8.0;net9.0 + net6.0;net8.0;net9.0;net10.0 True ..\..\signing\RulesEngine-publicKey.snk True diff --git a/test/RulesEngine.UnitTest/UtilsTests.cs b/test/RulesEngine.UnitTest/UtilsTests.cs index ef88ec4d..3dd21044 100644 --- a/test/RulesEngine.UnitTest/UtilsTests.cs +++ b/test/RulesEngine.UnitTest/UtilsTests.cs @@ -103,6 +103,120 @@ public void CreateAbstractType_dynamicObject() } + [Fact] + public void GetTypedObject_Dictionary_ReturnsTypedObject() + { + var dict = new Dictionary + { + { "Name", "Alice" }, + { "Age", 25 } + }; + + var result = Utils.GetTypedObject(dict); + Assert.IsNotType>(result); + Assert.NotNull(result.GetType().GetProperty("Name")); + Assert.NotNull(result.GetType().GetProperty("Age")); + } + + [Fact] + public void GetTypedObject_Dictionary_NestedDictionary() + { + var dict = new Dictionary + { + { "Name", "Alice" }, + { "Address", new Dictionary + { + { "City", "Seattle" }, + { "Zip", "98101" } + } + } + }; + + var result = Utils.GetTypedObject(dict); + Assert.IsNotType>(result); + var addressProp = result.GetType().GetProperty("Address"); + Assert.NotNull(addressProp); + var address = addressProp.GetValue(result); + Assert.NotNull(address.GetType().GetProperty("City")); + Assert.NotNull(address.GetType().GetProperty("Zip")); + } + + [Fact] + public void GetTypedObject_Dictionary_WithList() + { + var dict = new Dictionary + { + { "Name", "Alice" }, + { "Scores", new List { 90, 85, 92 } } + }; + + var result = Utils.GetTypedObject(dict); + Assert.IsNotType>(result); + Assert.NotNull(result.GetType().GetProperty("Name")); + Assert.NotNull(result.GetType().GetProperty("Scores")); + } + + [Fact] + public void GetTypedObject_Dictionary_WithEmptyList() + { + var dict = new Dictionary + { + { "Items", new List() } + }; + + var result = Utils.GetTypedObject(dict); + Assert.IsNotType>(result); + Assert.NotNull(result.GetType().GetProperty("Items")); + } + + [Fact] + public void GetTypedObject_Dictionary_WithNestedExpandoObject() + { + dynamic nested = new ExpandoObject(); + nested.Value = "test"; + + var dict = new Dictionary + { + { "Nested", (object)nested } + }; + + var result = Utils.GetTypedObject(dict); + Assert.IsNotType>(result); + var nestedProp = result.GetType().GetProperty("Nested"); + Assert.NotNull(nestedProp); + } + + [Fact] + public void GetTypedObject_Dictionary_WithListOfDictionaries() + { + var dict = new Dictionary + { + { "People", new List + { + new Dictionary { { "Name", "Alice" } }, + new Dictionary { { "Name", "Bob" } } + } + } + }; + + var result = Utils.GetTypedObject(dict); + Assert.IsNotType>(result); + Assert.NotNull(result.GetType().GetProperty("People")); + } + + [Fact] + public void GetTypedObject_Dictionary_WithNullValue() + { + var dict = new Dictionary + { + { "Name", "Alice" }, + { "Middle", null } + }; + + var result = Utils.GetTypedObject(dict); + Assert.IsNotType>(result); + Assert.NotNull(result.GetType().GetProperty("Name")); + } } }