diff --git a/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs b/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs index dff25653..f4138fc6 100644 --- a/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs +++ b/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs @@ -20,12 +20,61 @@ public class RuleExpressionParser { private readonly ReSettings _reSettings; private readonly IDictionary _methodInfo; + private readonly MemCache _compiledDelegateCache; + private readonly string _settingsFingerprint; public RuleExpressionParser(ReSettings reSettings = null) { _reSettings = reSettings ?? new ReSettings(); _methodInfo = new Dictionary(); PopulateMethodInfo(); + + var cacheConfig = _reSettings.CacheConfig ?? new MemCacheConfig(); + if (cacheConfig.SizeLimit > 0) + { + _compiledDelegateCache = new MemCache(cacheConfig); + _settingsFingerprint = BuildSettingsFingerprint(_reSettings); + } + } + + private static string BuildSettingsFingerprint(ReSettings settings) + { + var customTypesKey = settings.CustomTypes == null + ? "" + : string.Join(",", settings.CustomTypes.Where(t => t != null).Select(t => t.AssemblyQualifiedName)); + return $"cs={settings.IsExpressionCaseSensitive};fast={settings.UseFastExpressionCompiler};types={customTypesKey}"; + } + + private string BuildCompileCacheKey(string expression, RuleParameter[] ruleParams, Type returnType) + { + var paramKey = ruleParams == null + ? string.Empty + : string.Join("|", ruleParams.Select(c => c == null ? "" : c.Name + "_" + (c.Type?.AssemblyQualifiedName ?? ""))); + var returnTypeKey = returnType?.AssemblyQualifiedName ?? ""; + return $"S[{_settingsFingerprint}]|R[{returnTypeKey}]|P[{paramKey}]|E[{expression}]"; + } + + private string BuildScopedParamsCacheKey(RuleParameter[] ruleParams, RuleExpressionParameter[] ruleExpParams) + { + var paramKey = ruleParams == null + ? string.Empty + : string.Join("|", ruleParams.Select(c => c == null ? "" : c.Name + "_" + (c.Type?.AssemblyQualifiedName ?? ""))); + var scopedKey = ruleExpParams == null + ? string.Empty + : string.Join("|", ruleExpParams.Select(c => DescribeScopedParam(c))); + return $"SCOPED|S[{_settingsFingerprint}]|P[{paramKey}]|X[{scopedKey}]"; + } + + private static string DescribeScopedParam(RuleExpressionParameter expParam) + { + if (expParam?.ParameterExpression == null) + { + return ""; + } + var name = expParam.ParameterExpression.Name; + var type = expParam.ParameterExpression.Type?.AssemblyQualifiedName ?? ""; + var value = expParam.ValueExpression?.ToString() ?? ""; + return $"{name}_{type}=[{value}]"; } private void PopulateMethodInfo() @@ -70,6 +119,17 @@ private object GetDefaultValueForType(Type type) } public Func Compile(string expression, RuleParameter[] ruleParams) + { + if (_compiledDelegateCache == null) + { + return CompileInternal(expression, ruleParams); + } + + var cacheKey = BuildCompileCacheKey(expression, ruleParams, typeof(T)); + return _compiledDelegateCache.GetOrCreate(cacheKey, () => CompileInternal(expression, ruleParams)); + } + + private Func CompileInternal(string expression, RuleParameter[] ruleParams) { var rtype = typeof(T); if (rtype == typeof(object)) @@ -86,7 +146,6 @@ public Func Compile(string expression, RuleParameter[] ruleParam var expressionBody = new List() { e }; var wrappedExpression = WrapExpression(expressionBody, parameterExpressions, new ParameterExpression[] { }); return CompileExpression(wrappedExpression); - } private Func CompileExpression(Expression> expression) @@ -113,8 +172,16 @@ private Expression> WrapExpression(List express internal Func> CompileRuleExpressionParameters(RuleParameter[] ruleParams, RuleExpressionParameter[] ruleExpParams = null) { ruleExpParams = ruleExpParams ?? new RuleExpressionParameter[] { }; - var expression = CreateDictionaryExpression(ruleParams, ruleExpParams); - return CompileExpression(expression); + + if (_compiledDelegateCache == null) + { + return CompileExpression(CreateDictionaryExpression(ruleParams, ruleExpParams)); + } + + var cacheKey = BuildScopedParamsCacheKey(ruleParams, ruleExpParams); + return _compiledDelegateCache.GetOrCreate( + cacheKey, + () => CompileExpression(CreateDictionaryExpression(ruleParams, ruleExpParams))); } public T Evaluate(string expression, RuleParameter[] ruleParams) diff --git a/test/RulesEngine.UnitTest/RuleExpressionParserTests/RuleExpressionParserTests.cs b/test/RulesEngine.UnitTest/RuleExpressionParserTests/RuleExpressionParserTests.cs index 0be1fe93..883ede87 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.HelperFunctions; using RulesEngine.Models; using System.Diagnostics.CodeAnalysis; using Xunit; @@ -89,6 +90,47 @@ public void TestExpressionWithDictionaryParameter() Assert.True(resultEqual); } + [Fact] + public void Compile_SameExpressionAndParams_ReturnsCachedDelegate() + { + var parser = new RuleExpressionParser(new ReSettings()); + var ruleParams = new[] { RuleParameter.Create("x", 1) }; + + var first = parser.Compile("x > 0", ruleParams); + var second = parser.Compile("x > 0", ruleParams); + + Assert.Same(first, second); + Assert.True(first(new object[] { 5 })); + } + + [Fact] + public void Compile_DifferentSettings_DoNotShareCache() + { + var caseInsensitive = new RuleExpressionParser(new ReSettings { IsExpressionCaseSensitive = false }); + var caseSensitive = new RuleExpressionParser(new ReSettings { IsExpressionCaseSensitive = true }); + var ruleParams = new[] { RuleParameter.Create("x", 1) }; + + var insensitiveDelegate = caseInsensitive.Compile("x > 0", ruleParams); + var sensitiveDelegate = caseSensitive.Compile("x > 0", ruleParams); + + Assert.NotSame(insensitiveDelegate, sensitiveDelegate); + Assert.True(insensitiveDelegate(new object[] { 5 })); + Assert.True(sensitiveDelegate(new object[] { 5 })); + } + + [Fact] + public void Compile_CacheDisabled_RecompilesEveryCall() + { + var parser = new RuleExpressionParser(new ReSettings { CacheConfig = new MemCacheConfig { SizeLimit = 0 } }); + var ruleParams = new[] { RuleParameter.Create("x", 1) }; + + var first = parser.Compile("x > 0", ruleParams); + var second = parser.Compile("x > 0", ruleParams); + + Assert.NotSame(first, second); + Assert.True(first(new object[] { 5 })); + } + [Fact] public void TestExpressionWithDictionaryParameter_MultipleKeys() {