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
20 changes: 19 additions & 1 deletion src/RulesEngine/Actions/ExpressionOutputAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@

using RulesEngine.ExpressionBuilders;
using RulesEngine.Models;
using System;
using System.Linq.Dynamic.Core.Exceptions;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace RulesEngine.Actions
{
public class OutputExpressionAction : ActionBase
{
private static readonly Regex CSharpAnonymousObjectPattern =
new Regex(@"\bnew\s*\{", RegexOptions.Compiled);

private readonly RuleExpressionParser _ruleExpressionParser;

public OutputExpressionAction(RuleExpressionParser ruleExpressionParser)
Expand All @@ -19,7 +25,19 @@ public OutputExpressionAction(RuleExpressionParser ruleExpressionParser)
public override ValueTask<object> Run(ActionContext context, RuleParameter[] ruleParameters)
{
var expression = context.GetContext<string>("expression");
return new ValueTask<object>(_ruleExpressionParser.Evaluate<object>(expression, ruleParameters));
try
{
return new ValueTask<object>(_ruleExpressionParser.Evaluate<object>(expression, ruleParameters));
}
catch (ParseException ex) when (CSharpAnonymousObjectPattern.IsMatch(expression ?? string.Empty))
{
throw new ParseException(
"OutputExpression failed to parse. It looks like the expression uses C#-style anonymous-object syntax " +
"(`new { Name = value, ... }`), which is not supported by System.Linq.Dynamic.Core. " +
"Use the Dynamic.Core form instead: `new (value as Name, ...)` — parentheses, and each field needs an `as Alias`. " +
"Original parser error: " + ex.Message,
ex.Position);
}
}
}
}
17 changes: 15 additions & 2 deletions src/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,24 @@ internal override RuleFunc<RuleResultTree> BuildDelegateForRule(Rule rule, RuleP
{
Helpers.HandleRuleException(ex,rule,_reSettings);

var exceptionMessage = Helpers.GetExceptionMessage($"Exception while parsing expression `{rule?.Expression}` - {ex.Message}",
var detail = ex.Message;
if (detail != null
&& (detail.Contains("exists in type 'Object'")
|| detail.Contains("'System.Object'"))
&& (rule?.Expression?.Contains('(') == true))
{
// Dynamic.Core can only resolve members and operators against a static return type.
// If a custom/static method's declared return type is `object`, member access or
// operator usage on its result fails. See #717.
detail += " (Hint: a method called in this expression appears to have an `object` return type. " +
"Change its return type to the concrete class — Dynamic.Core cannot resolve members or operators on `object`.)";
}

var exceptionMessage = Helpers.GetExceptionMessage($"Exception while parsing expression `{rule?.Expression}` - {detail}",
_reSettings);

bool func(object[] param) => false;

return Helpers.ToResultTree(_reSettings, rule, null,func, exceptionMessage);
}
}
Expand Down
80 changes: 59 additions & 21 deletions src/RulesEngine/HelperFunctions/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,7 @@ public static Type CreateAbstractClassType(dynamic input)
Type value;
if (expando.Value is IList list)
{
if (list.Count == 0)
{
value = typeof(List<object>);
}
else
{
var internalType = CreateAbstractClassType(list[0]);
value = new List<object>().Cast(internalType).ToList(internalType).GetType();
}

value = BuildListType(list);
}
else
{
Expand All @@ -77,6 +68,63 @@ public static Type CreateAbstractClassType(dynamic input)
return type;
}

// Returns the CLR List<T> type that should represent a heterogeneous IList of ExpandoObject /
// IDictionary<string, object> elements. Walks every element so properties that only appear in
// later elements are still included in the generated type. See #704.
private static Type BuildListType(IList list)
{
if (list.Count == 0)
{
return typeof(List<object>);
}

var firstElement = list[0];
if (firstElement is ExpandoObject || firstElement is IDictionary<string, object>)
{
var merged = MergeDictionaries(list.OfType<IDictionary<string, object>>());
var internalType = CreateAbstractClassTypeFromDictionary(merged);
return new List<object>().Cast(internalType).ToList(internalType).GetType();
}

// Non-schema-like element: fall back to first-element type as before.
var legacyType = CreateAbstractClassType(firstElement);
return new List<object>().Cast(legacyType).ToList(legacyType).GetType();
}

// Unions schemas from any number of dict-like inputs. Used both to merge sibling
// elements of a heterogeneous list (#704) and to merge nested dicts recursively.
private static IDictionary<string, object> MergeDictionaries(IEnumerable<IDictionary<string, object>> dictionaries)
{
var merged = new Dictionary<string, object>();
foreach (var dict in dictionaries)
{
foreach (var kvp in dict)
{
merged[kvp.Key] = merged.TryGetValue(kvp.Key, out var existing)
? MergeValues(existing, kvp.Value)
: kvp.Value;
}
}
return merged;
}

private static object MergeValues(object existing, object incoming)
{
if (existing is IDictionary<string, object> a && incoming is IDictionary<string, object> b)
{
return MergeDictionaries(new[] { a, b });
}
if (existing is IList la && incoming is IList lb)
{
var combined = new List<object>();
foreach (var e in la) combined.Add(e);
foreach (var e in lb) combined.Add(e);
return combined;
}
// First non-null wins on type conflict.
return existing ?? incoming;
}

public static object CreateObject(Type type, dynamic input)
{
if (input is not ExpandoObject expandoObject)
Expand Down Expand Up @@ -152,17 +200,7 @@ private static Type CreateAbstractClassTypeFromDictionary(IDictionary<string, ob
}
else if (kvp.Value is IList list)
{
if (list.Count == 0)
{
valueType = typeof(List<object>);
}
else
{
var internalType = list[0] is IDictionary<string, object> innerDict
? CreateAbstractClassTypeFromDictionary(innerDict)
: (list[0] is ExpandoObject ? CreateAbstractClassType(list[0]) : list[0]?.GetType() ?? typeof(object));
valueType = new List<object>().Cast(internalType).ToList(internalType).GetType();
}
valueType = BuildListType(list);
}
else
{
Expand Down
8 changes: 4 additions & 4 deletions src/RulesEngine/RuleCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ internal RuleFunc<RuleResultTree> CompileRule(Rule rule, RuleExpressionType rule
var globalParamExp = globalParams.Value;
var extendedRuleParams = ruleParams.Concat(globalParamExp.Select(c => new RuleParameter(c.ParameterExpression.Name,c.ParameterExpression.Type)))
.ToArray();
var ruleExpression = GetDelegateForRule(rule, extendedRuleParams);


return GetWrappedRuleFunc(rule,ruleExpression,ruleParams,globalParamExp);
// Note: globals are no longer evaluated here per rule. The caller is expected
// to evaluate workflow-level globals once and pass them as extra RuleParameters
// when invoking the returned delegate. See #714.
return GetDelegateForRule(rule, extendedRuleParams);
}
catch (Exception ex)
{
Expand Down
20 changes: 19 additions & 1 deletion src/RulesEngine/RulesCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,29 @@ internal class RulesCache
/// <summary>The compile rules</summary>
private readonly MemCache _compileRules;

/// <summary>Per-workflow compiled delegate that evaluates all global params once.</summary>
private readonly MemCache _compiledGlobalParams;

/// <summary>The workflow rules</summary>
private readonly ConcurrentDictionary<string, (Workflow, long)> _workflow = new ConcurrentDictionary<string, (Workflow, long)>();


public RulesCache(ReSettings reSettings)
{
_compileRules = new MemCache(reSettings.CacheConfig);
_compiledGlobalParams = new MemCache(reSettings.CacheConfig);
}

/// <summary>Adds or updates the workflow-level global-params delegate.</summary>
public void AddOrUpdateGlobalParamsDelegate(string compiledRuleKey, Func<object[], Dictionary<string, object>> globalParamsDelegate)
{
_compiledGlobalParams.Set(compiledRuleKey, globalParamsDelegate);
}

/// <summary>Gets the workflow-level global-params delegate, or null if the workflow has no globals.</summary>
public Func<object[], Dictionary<string, object>> GetGlobalParamsDelegate(string compiledRuleKey)
{
return _compiledGlobalParams.Get<Func<object[], Dictionary<string, object>>>(compiledRuleKey);
}


Expand Down Expand Up @@ -81,6 +97,7 @@ public void Clear()
{
_workflow.Clear();
_compileRules.Clear();
_compiledGlobalParams.Clear();
}

/// <summary>Gets the work flow rules.</summary>
Expand Down Expand Up @@ -133,10 +150,11 @@ public void Remove(string workflowName)
{
if (_workflow.TryRemove(workflowName, out var workflowObj))
{
var compiledKeysToRemove = _compileRules.GetKeys().Where(key => key.StartsWith(workflowName));
var compiledKeysToRemove = _compileRules.GetKeys().Where(key => key.StartsWith(workflowName)).ToList();
foreach (var key in compiledKeysToRemove)
{
_compileRules.Remove(key);
_compiledGlobalParams.Remove(key);
}
}
}
Expand Down
Loading
Loading