diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c1ce981..75f34c2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. +## [5.0.1] +- Added option to disable automatic type registry for input parameters in reSettings +- Added option to make expression case sensitive in reSettings + ## [5.0.0] - Fixed security bug related to System.Dynamic.Linq.Core diff --git a/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs b/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs index 8570fdbb..3d169190 100644 --- a/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs +++ b/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs @@ -17,15 +17,11 @@ namespace RulesEngine.ExpressionBuilders public class RuleExpressionParser { private readonly ReSettings _reSettings; - private static MemCache _memoryCache; private readonly IDictionary _methodInfo; public RuleExpressionParser(ReSettings reSettings) { _reSettings = reSettings; - _memoryCache = _memoryCache ?? new MemCache(new MemCacheConfig { - SizeLimit = 1000 - }); _methodInfo = new Dictionary(); PopulateMethodInfo(); } @@ -38,7 +34,8 @@ private void PopulateMethodInfo() public Expression Parse(string expression, ParameterExpression[] parameters, Type returnType) { var config = new ParsingConfig { - CustomTypeProvider = new CustomTypeProvider(_reSettings.CustomTypes) + CustomTypeProvider = new CustomTypeProvider(_reSettings.CustomTypes), + IsCaseSensitive = _reSettings.IsExpressionCaseSensitive }; return new ExpressionParser(parameters, expression, new object[] { }, config).Parse(returnType); @@ -51,19 +48,17 @@ public Func Compile(string expression, RuleParameter[] ruleParam { rtype = null; } - var cacheKey = GetCacheKey(expression, ruleParams, typeof(T)); - return _memoryCache.GetOrCreate(cacheKey, () => { - var parameterExpressions = GetParameterExpression(ruleParams).ToArray(); + var parameterExpressions = GetParameterExpression(ruleParams).ToArray(); - var e = Parse(expression, parameterExpressions, rtype); - if(rtype == null) - { - e = Expression.Convert(e, typeof(T)); - } - var expressionBody = new List() { e }; - var wrappedExpression = WrapExpression(expressionBody, parameterExpressions, new ParameterExpression[] { }); - return wrappedExpression.CompileFast(); - }); + var e = Parse(expression, parameterExpressions, rtype); + if(rtype == null) + { + e = Expression.Convert(e, typeof(T)); + } + var expressionBody = new List() { e }; + var wrappedExpression = WrapExpression(expressionBody, parameterExpressions, new ParameterExpression[] { }); + return wrappedExpression.CompileFast(); + } private Expression> WrapExpression(List expressionList, ParameterExpression[] parameters, ParameterExpression[] variables) @@ -155,13 +150,5 @@ private Expression>> CreateDictionaryExp return WrapExpression>(body, paramExp.ToArray(), variableExp.ToArray()); } - - private string GetCacheKey(string expression, RuleParameter[] ruleParameters, Type returnType) - { - var paramKey = string.Join("|", ruleParameters.Select(c => c.Name + "_" + c.Type.ToString())); - var returnTypeKey = returnType?.ToString() ?? "null"; - var combined = $"Expression:{expression}-Params:{paramKey}-ReturnType:{returnTypeKey}"; - return combined; - } } } diff --git a/src/RulesEngine/Models/ReSettings.cs b/src/RulesEngine/Models/ReSettings.cs index 8589092e..7a133ea6 100644 --- a/src/RulesEngine/Models/ReSettings.cs +++ b/src/RulesEngine/Models/ReSettings.cs @@ -12,7 +12,6 @@ namespace RulesEngine.Models [ExcludeFromCodeCoverage] public class ReSettings { - public ReSettings() { } // create a copy of settings @@ -26,7 +25,9 @@ internal ReSettings(ReSettings reSettings) EnableScopedParams = reSettings.EnableScopedParams; NestedRuleExecutionMode = reSettings.NestedRuleExecutionMode; CacheConfig = reSettings.CacheConfig; - } + IsExpressionCaseSensitive = reSettings.IsExpressionCaseSensitive; + AutoRegisterInputType = reSettings.AutoRegisterInputType; + } /// @@ -62,6 +63,17 @@ internal ReSettings(ReSettings reSettings) /// public bool EnableScopedParams { get; set; } = true; + /// + /// Sets whether expression are case sensitive + /// + public bool IsExpressionCaseSensitive { get; set; } = false; + + /// + /// Auto Registers input type in Custom Type to allow calling method on type. + /// Default : true + /// + public bool AutoRegisterInputType { get; set; } = true; + /// /// Sets the mode for Nested rule execution, Default: All /// diff --git a/src/RulesEngine/RuleCompiler.cs b/src/RulesEngine/RuleCompiler.cs index d05bce88..4a036039 100644 --- a/src/RulesEngine/RuleCompiler.cs +++ b/src/RulesEngine/RuleCompiler.cs @@ -241,7 +241,7 @@ private RuleFunc GetWrappedRuleFunc(Rule rule, RuleFunc { var inputs = ruleParams.Select(c => c.Value).ToArray(); diff --git a/src/RulesEngine/RulesEngine.cs b/src/RulesEngine/RulesEngine.cs index 9002f82e..66cd352b 100644 --- a/src/RulesEngine/RulesEngine.cs +++ b/src/RulesEngine/RulesEngine.cs @@ -287,7 +287,10 @@ private bool RegisterRule(string workflowName, params RuleParameter[] ruleParams if (workflow != null) { var dictFunc = new Dictionary>(); - _reSettings.CustomTypes = _reSettings.CustomTypes.Safe().Union(ruleParams.Select(c => c.Type)).ToArray(); + if (_reSettings.AutoRegisterInputType) + { + _reSettings.CustomTypes = _reSettings.CustomTypes.Safe().Union(ruleParams.Select(c => c.Type)).ToArray(); + } // add separate compilation for global params var globalParamExp = new Lazy( diff --git a/src/RulesEngine/RulesEngine.csproj b/src/RulesEngine/RulesEngine.csproj index 80f3998f..77879a4e 100644 --- a/src/RulesEngine/RulesEngine.csproj +++ b/src/RulesEngine/RulesEngine.csproj @@ -2,7 +2,7 @@ net6.0;netstandard2.0 - 5.0.0 + 5.0.1 Copyright (c) Microsoft Corporation. LICENSE https://github.com/microsoft/RulesEngine diff --git a/test/RulesEngine.UnitTest/ActionTests/MockClass/ReturnContextAction.cs b/test/RulesEngine.UnitTest/ActionTests/MockClass/ReturnContextAction.cs index 174a8490..3ee98c30 100644 --- a/test/RulesEngine.UnitTest/ActionTests/MockClass/ReturnContextAction.cs +++ b/test/RulesEngine.UnitTest/ActionTests/MockClass/ReturnContextAction.cs @@ -5,11 +5,13 @@ using RulesEngine.Models; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text; using System.Threading.Tasks; namespace RulesEngine.UnitTest.ActionTests.MockClass { + [ExcludeFromCodeCoverage] public class ReturnContextAction : ActionBase { public override ValueTask Run(ActionContext context, RuleParameter[] ruleParameters) diff --git a/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs index 521741df..93b7aac5 100644 --- a/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs +++ b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs @@ -254,6 +254,23 @@ public void RulesEngine_New_IncorrectJSON_ThrowsException() } + [Fact] + public void RulesEngine_AddOrUpdate_IncorrectJSON_ThrowsException() + { + Assert.Throws(() => { + var workflow = new Workflow(); + var re = new RulesEngine(); + re.AddOrUpdateWorkflow(workflow); + }); + + Assert.Throws(() => { + var workflow = new Workflow() { WorkflowName = "test" }; + var re = new RulesEngine(); + re.AddOrUpdateWorkflow(workflow); + }); + } + + [Theory] [InlineData("rules1.json")] public async Task ExecuteRule_InvalidWorkFlow_ThrowsException(string ruleFileName) diff --git a/test/RulesEngine.UnitTest/CaseSensitiveTests.cs b/test/RulesEngine.UnitTest/CaseSensitiveTests.cs new file mode 100644 index 00000000..8f123038 --- /dev/null +++ b/test/RulesEngine.UnitTest/CaseSensitiveTests.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine.Models; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace RulesEngine.UnitTest +{ + [ExcludeFromCodeCoverage] + public class CaseSensitiveTests + { + [Theory] + [InlineData(true,true,false)] + [InlineData(false,true,true)] + + public async Task CaseSensitiveTest(bool caseSensitive, bool expected1, bool expected2) + { + var reSettings = new ReSettings { + IsExpressionCaseSensitive = caseSensitive + }; + + + var worflow = new Workflow { + WorkflowName = "CaseSensitivityTest", + Rules = new[] { + new Rule { + RuleName = "check same case1", + Expression = "input1 == \"hello\"" + }, + new Rule { + RuleName = "check same case2", + Expression = "INPUT1 == \"hello\"" + } + } + }; + + var re = new RulesEngine(new[] { worflow }, reSettings); + var result = await re.ExecuteAllRulesAsync("CaseSensitivityTest", "hello"); + + Assert.Equal(expected1, result[0].IsSuccess); + Assert.Equal(expected2, result[1].IsSuccess); + } + } +} diff --git a/test/RulesEngine.UnitTest/TestData/rules4.json b/test/RulesEngine.UnitTest/TestData/rules4.json index 4f11a8f8..63bb1ca0 100644 --- a/test/RulesEngine.UnitTest/TestData/rules4.json +++ b/test/RulesEngine.UnitTest/TestData/rules4.json @@ -40,7 +40,7 @@ { "RuleName": "GiveDiscount25", "SuccessEvent": "25", - "ErrorMessage": "One or more adjust rules failed, country : $(input4.country), loyaltyFactor : $(input4.loyaltyFactor), totalPurchasesToDate : $(input4.totalPurchasesToDate), totalOrders : $(input5.totalOrders), noOfVisitsPerMonth : $(input30.noOfVisitsPerMonth)", + "ErrorMessage": "One or more adjust rules failed, country : $(input4.country), loyaltyFactor : $(input4.loyaltyFactor), totalPurchasesToDate : $(input4.totalPurchasesToDate), totalOrders : $(input5.totalOrders), noOfVisitsPerMonth : $(input30.noOfVisitsPerMonth), $(model2)", "ErrorType": "Error", "localParams": [ { diff --git a/test/RulesEngine.UnitTest/TypedClassTests.cs b/test/RulesEngine.UnitTest/TypedClassTests.cs new file mode 100644 index 00000000..bb1fd130 --- /dev/null +++ b/test/RulesEngine.UnitTest/TypedClassTests.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine.Models; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace RulesEngine.UnitTest +{ + [Trait("Category", "Unit")] + [ExcludeFromCodeCoverage] + public class TypedClassTests + { + public class Transazione + { + public List Attori { get; set; } = new(); + } + public class Attore + { + public Guid Id { get; internal set; } + public string Nome { get; internal set; } + public RuoloAttore RuoloAttore { get; internal set; } + } + + public enum RuoloAttore + { + A, + B, + C + } + + [Fact] + public async Task TypedClassTest() + { + Workflow workflow = new() { + WorkflowName = "Conferimento", + Rules = new Rule[] { + new() { + RuleName = "Attore Da", + Enabled = true, + ErrorMessage = "Attore Da Id must be defined", + SuccessEvent = "10", + RuleExpressionType = RuleExpressionType.LambdaExpression, + Expression = "transazione.Attori.Any(a => a.RuoloAttore == 1)", + }, + new() { + RuleName = "Attore A", + Enabled = true, + ErrorMessage = "Attore A must be defined", + SuccessEvent = "10", + RuleExpressionType = RuleExpressionType.LambdaExpression, + Expression = "transazione.Attori != null", + }, + } + }; + var reSettings = new ReSettings() { + CustomTypes = new Type[] { + }, + AutoRegisterInputType = false + }; + var re = new RulesEngine(reSettings); + re.AddWorkflow(workflow); + + var param = new Transazione { + Attori = new List{ + new Attore{ + RuoloAttore = RuoloAttore.B, + + }, + new Attore { + RuoloAttore = RuoloAttore.C + } + } + + }; + + var result = await re.ExecuteAllRulesAsync("Conferimento", new RuleParameter("transazione", param)); + + Assert.All(result, (res) => Assert.True(res.IsSuccess)); + + } + } +}