RulesEngine icon indicating copy to clipboard operation
RulesEngine copied to clipboard

Lambda variable identifier treated as an unknown identifier

Open BardezAnAvatar opened this issue 1 year ago • 9 comments

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 avatar May 15 '24 17:05 BardezAnAvatar

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 avatar May 15 '24 21:05 BardezAnAvatar

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 avatar May 15 '24 21:05 BardezAnAvatar

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);
    }

BardezAnAvatar avatar May 16 '24 05:05 BardezAnAvatar

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

asulwer avatar Jun 21 '24 00:06 asulwer

@BardezAnAvatar, I'm having the same issue. I didn't quite get what you eventually did to resolve it. Thank you!

maqduni avatar Oct 10 '24 23:10 maqduni

I had to use the casting syntax for DynamicLINQ, to ensure that the objects are evaluated explicitly as interfaces and not the objects that tbey are. So, by casting to an interface, they use the interface properties, not the class properties.

BardezAnAvatar avatar Oct 11 '24 00:10 BardezAnAvatar

Where does this casting happen? Do you do it directly in this LINQ expression line.Modifiers.Any(l => new [] {"25"}.Contains(l))? If so, could you provide an example of how it's done?

maqduni avatar Oct 11 '24 01:10 maqduni

You use the As({var}, {type}) syntax wherever you reference the variable. You could probably create a scoped param at top level that does this once for you instead, creating a variable CastedVar that gets used instead of line.

BardezAnAvatar avatar Oct 11 '24 12:10 BardezAnAvatar

this fork is no longer maintained, please move your issue to a fork that is maintained

asulwer avatar Oct 14 '24 15:10 asulwer