microsoft / RulesEngine

A Json based Rules Engine with extensive Dynamic expression support
https://microsoft.github.io/RulesEngine/
MIT License
3.47k stars 528 forks source link

Lambda variable identifier treated as an unknown identifier #606

Open BardezAnAvatar opened 1 month ago

BardezAnAvatar commented 1 month ago

I've an interesting scenario that is driving me nuts:

I have a model that I am registering to RulesEngine via parameter (line) that consists of: IReadOnlyList<string> Modifiers { get; }

Any expression that accesses this (in a .NET 6 project/solution) is encountering the following:

Exception while parsing expression `line.Modifiers.Any(l => new [] {"25"}.Contains(l))` - Unknown identifier 'l'

Oddly, in a separate solution that generates this expression, I am able to load the above without issue in my integration test project. Taking the same expression over to my other solution, however, yields the above exception when running RulesEngine. Both solutions use the exact same NuGet version of RulesEngine (5.0.3), using the same Workflow serialization/deserialization.

With tinkering, it seems that the specific reference of l that is a problem is on the left-hand side of the lambda, not the .Contains(l) statement.

BardezAnAvatar commented 1 month ago

Root cause has been identified:

I have a (very) complex object called ClaimDto, which has its own implementations. One child object is the ClaimLine, as referenced above:

public interface IClaimLine
{
  [redacted]

  IReadOnlyList<string> Modifiers { get; }

  [redacted]
}

public class ClaimLine : IClaimLine
{
    [redacted]

    //The original property
    public ClaimLineModifiers Modifiers => _modifiers;

    //The interface's property
    IReadOnlyList<string> IClaimLine.Modifiers => _modifiers.Items().Select(c => c.Modifier).ToList();

    [redacted]
}

public class ClaimLineModifiers : GenericImmutableArray<ClaimLineModifier>
{
    [redacted]
}

public class ClaimLineModifier
{
    [redacted]

    public string Modifier {get; set;}

    [redacted]
}

It turns out that the RuleParameter does not take the type of the reference being passed around (IClaim/IClaimLine) into account, but instead uses the object's type when evaluating the rule.

I uncovered this when screwing around with the expression:

Exception while parsing expression `line.Modifiers.ToList().Any(l => new [] {"25"}.Contains(l))` - Methods on type 'GenericImmutableArray`1' are not accessible

what the [expletive]? why is line.Modifiers pinging as GenericImmutableArray??

I also tried by renaming my interface's Modifiers to Modifiers2 and referencing the property there; no such property when evaluating against Modifiers2 if it remained an explicit interface implementation.

I suppose my immediate solution would be to cast my line to IClaimLine inside of my expression.

BardezAnAvatar commented 1 month ago

Oddly, in a separate solution that generates this expression, I am able to load the above without issue in my integration test project. Taking the same expression over to my other solution, however, yields the above exception when running RulesEngine. Both solutions use the exact same NuGet version of RulesEngine (5.0.3), using the same Workflow serialization/deserialization

The reason that Solution A (source of the expression, with tests) worked was because Solution A only had references to the interface and used mock objects to test expressions in RulesEngine locally. Solution B consuming the expressions have the complicated object model, which has conflicting types between the interface's .Modifiers and the class' .Modifiers

BardezAnAvatar commented 1 month ago

I've got a resolution for my solution: I need to do the following for single-instance objects:

As(line, "<<<FullyQualifiedNamespace>>>.IClaimLine")

and for lists:

history.Cast("<<<FullyQualifiedNamespace>>>.IClaim")

I would raise that a suggested feature request would be to specify the type to evaluate a given object as for an additional RuleParameter.Create. This would allow the expected behavior of passing in an IClaim to evaluate as an IClaim rather than an instance with a complicated and messy legacy model implementation.

It looks like there is such a constructor on RuleParameter, but it is internal:

    internal RuleParameter(string name, Type type, object value = null)
    {
        Value = Utils.GetTypedObject(value);
        Init(name, type);
    }
asulwer commented 1 week ago

i made some changes to my fork which may affect this question