From 7ea3cbdcc2543ab2ade338258761631d3987c781 Mon Sep 17 00:00:00 2001 From: Ben Luersen Date: Wed, 10 Jun 2026 16:14:50 -0400 Subject: [PATCH] Fix warmup regression: avoid per-expression AppDomain assembly scan Since 71d59dc, CustomTypeProvider.GetCustomTypes() includes base.GetCustomTypes(), which scans every assembly in the AppDomain for [DynamicLinqType] types. DefaultDynamicLinqCustomTypeProvider only caches that scan per provider instance, and RuleExpressionParser.Parse built a new ParsingConfig + CustomTypeProvider for every expression parsed, so the scan ran for every expression in every rule (KeywordsHelper enumerates the full type set on each ExpressionParser construction). For workflows with thousands of rules this regressed warmup ~6.6x versus 5.0.3 (113.8s vs 17.3s for 20,000 rules with local params and 174 loaded assemblies). The compiled-delegate cache from #727 does not help here because each rule expression is unique. Fix: - Reuse one ParsingConfig/CustomTypeProvider across parses, rebuilding only when ReSettings.CustomTypes is swapped (AutoRegisterInputType replaces the array on workflow registration). - Memoize the merged custom-type set in CustomTypeProvider; it is fixed after construction but was rebuilt (including the assembly scan) on every GetCustomTypes() call. With this change the same 20,000-rule benchmark warms up in 16.3s, matching 5.0.3, with identical rule results. Addresses the remaining root cause behind #707. Co-Authored-By: Claude Fable 5 --- src/RulesEngine/CustomTypeProvider.cs | 14 +++++++-- .../RuleExpressionParser.cs | 29 ++++++++++++++++--- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/RulesEngine/CustomTypeProvider.cs b/src/RulesEngine/CustomTypeProvider.cs index 58bbcbe7..81ff9ce7 100644 --- a/src/RulesEngine/CustomTypeProvider.cs +++ b/src/RulesEngine/CustomTypeProvider.cs @@ -42,11 +42,19 @@ public CustomTypeProvider(Type[] types) : base(ParsingConfig.Default) _types.Add(typeof(IEnumerable)); } + private HashSet _mergedTypes; + public override HashSet GetCustomTypes() { - var all = new HashSet(base.GetCustomTypes()); - all.UnionWith(_types); - return all; + // base.GetCustomTypes() scans every assembly in the AppDomain for [DynamicLinqType]. + // The provider's type set is fixed after construction, so merge exactly once. + if (_mergedTypes == null) + { + var all = new HashSet(base.GetCustomTypes()); + all.UnionWith(_types); + _mergedTypes = all; + } + return _mergedTypes; } } } diff --git a/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs b/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs index f4138fc6..1c25c167 100644 --- a/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs +++ b/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs @@ -83,12 +83,33 @@ private void PopulateMethodInfo() _methodInfo.Add("dict_add", dict_add); } + private ParsingConfig _cachedParsingConfig; + private Type[] _cachedParsingConfigCustomTypes; + + private ParsingConfig GetParsingConfig() + { + // Building a CustomTypeProvider is expensive: System.Linq.Dynamic.Core's + // DefaultDynamicLinqCustomTypeProvider scans all AppDomain assemblies for + // [DynamicLinqType] and only caches per provider instance. Reuse one config + // until ReSettings.CustomTypes is swapped (AutoRegisterInputType does this + // on workflow registration). + var customTypes = _reSettings.CustomTypes; + var config = _cachedParsingConfig; + if (config == null || !ReferenceEquals(_cachedParsingConfigCustomTypes, customTypes)) + { + config = new ParsingConfig { + CustomTypeProvider = new CustomTypeProvider(customTypes), + IsCaseSensitive = _reSettings.IsExpressionCaseSensitive + }; + _cachedParsingConfigCustomTypes = customTypes; + _cachedParsingConfig = config; + } + return config; + } + public Expression Parse(string expression, ParameterExpression[] parameters, Type returnType) { - var config = new ParsingConfig { - CustomTypeProvider = new CustomTypeProvider(_reSettings.CustomTypes), - IsCaseSensitive = _reSettings.IsExpressionCaseSensitive - }; + var config = GetParsingConfig(); // Instead of immediately returning default values, allow for expression parsing to handle dynamic evaluation. try