microsoft / RulesEngine

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

Problem with ScopedParams and null values #481

Closed ghigad closed 1 year ago

ghigad commented 1 year ago

Hi,

I use RulesEngine, version 4.0.0 with .NET Core 3.1.

I have a bunch of rules that I simplified using GlobalParams and LocalParams.

The problem I have is that my params refer to array properties of a child object, but, in some situations, this child object may be null. When this occurs, the whole workflow breaks down instead of returning valid values. I would like to know what would be the best practice in that situation.

Here is an example of the input data, the rules I use and the output I get... Please note that I have simplified everything to the smallest meaningful sample... Our real-life objects are much more complex and I have lots of rules, so reusing logic with GlobalParams and LocalParams is a life saver!

Input object:

{
    "ActivityId": 1,
    "Description": "Activity Description",
    "Quote": {
        "QuoteId": 1234,
        "Items": [{
                "ItemId": 1,
                "Description": "Product 1",
                "Quantity": 2,
                "UnitPrice": 1.00
            },
            {
                "ItemId": 2,
                "Description": "Product 2",
                "Quantity": 1,
                "UnitPrice": 10.00
            },
        ]
    }
}

Here is the workflow I use:

[
    {
        "WorkflowName": "Workflow",
        "WorkflowRulesToInject": null,
        "GlobalParams": [
            {
                "Name": "PiecesCount",
                "Expression": "Activity.Quote == null ? 0d : Activity.Quote.Items.Sum(Quantity)"
            }
        ],
        "Rules": [
            {
                "RuleName": "Load/unload pieces",
                "Operator": null,
                "ErrorMessage": "Rule failed",
                "RuleExpressionType": "LambdaExpression",
                "Expression": "PiecesCount != 0",
                "Actions": {
                    "OnSuccess": {
                        "Name": "OutputExpression",
                        "Context": {
                            "Expression": "PiecesCount * 90d"
                        }
                    }
                },
                "Rules": null
            }
        ]
    }
]

For completeness sake, here is how the workflow is run. The rules variable holds my workflow and the activity variable holds my input data...

var rulesEngine = new RulesEngine.RulesEngine(rules, new ReSettings()
{
    CustomTypes = null,
});

var result = await rulesEngine.ExecuteAllRulesAsync("Workflow",
                                                    new RuleParameter("Activity", activity));

When I run this workflow with the sample object provided, The PiecesCount is 3 and the OutputExpression is 270d, as expected.

In some cases, my "activities" have no Quote, like so:

{
    "ActivityId": 1,
    "Description": "Activity Description",
    "Quote": null
}

In that case, I have this error message when the Rule is run:

Error while compiling rule Load/unload pieces: No applicable method 'Sum' exists in type 'Object', in ScopedParam: PiecesCount

I would expect the rule to return 0d, since Activity.Quote == null (as a reminder, the GlobalParam's expression is Activity.Quote == null ? 0d : Activity.Quote.Items.Sum(Quantity)), but it seems the RulesEngine has interpreted the whole expression and failed because the Quote is null.

If my workflow has many rules and some rules never refer to my GlobalParam, all rules fail, even those who don't refer the param...

For instance, all rules of this workflow fail with the same error message:

[
    {
        "WorkflowName": "Workflow",
        "WorkflowRulesToInject": null,
        "GlobalParams": [
            {
                "Name": "PiecesCount",
                "Expression": "Activity.Quote == null ? 0d : Activity.Quote.Items.Sum(Quantity)"
            }
        ],
        "Rules": [
            {
                "RuleName": "Load/unload pieces",
                "Operator": null,
                "ErrorMessage": "Rule failed",
                "RuleExpressionType": "LambdaExpression",
                "Expression": "PiecesCount != 0",
                "Actions": {
                    "OnSuccess": {
                        "Name": "OutputExpression",
                        "Context": {
                            "Expression": "PiecesCount * 90d"
                        }
                    }
                },
                "Rule": null
            },
            {
                "RuleName": "Other unrelated rule",
                "Operator": null,
                "ErrorMessage": "Rule failed",
                "RuleExpressionType": "LambdaExpression",
                "Expression": "true == true",
                "Actions": {
                    "OnSuccess": {
                        "Name": "OutputExpression",
                        "Context": {
                            "Expression": "10d"
                        }
                    }
                },
                "Rules": null
            }
        ]
    }
]

Here is the complete response:

[
  {
    "Rule": {
      "RuleName": "Load/unload pieces",
      "Properties": null,
      "Operator": null,
      "ErrorMessage": "Rule failed",
      "Enabled": true,
      "RuleExpressionType": "LambdaExpression",
      "WorkflowsToInject": null,
      "Rules": null,
      "LocalParams": null,
      "Expression": "PiecesCount != 0",
      "Actions": {
        "OnSuccess": {
          "Name": "OutputExpression",
          "Context": {
            "Expression": "PiecesCount * 90d"
          }
        },
        "OnFailure": null
      },
      "SuccessEvent": null
    },
    "IsSuccess": false,
    "ChildResults": null,
    "Inputs": {
      "Activity": {
        "ActivityId": 1,
        "Description": "Activity Description",
        "Quote": null
      }
    },
    "ActionResult": {
      "Output": null,
      "Exception": null
    },
    "ExceptionMessage": "Error while compiling rule `Load/unload pieces`: No applicable method 'Sum' exists in type 'Object', in ScopedParam: PiecesCount"
  },
  {
    "Rule": {
      "RuleName": "Other unrelated rule",
      "Properties": null,
      "Operator": null,
      "ErrorMessage": "Rule failed",
      "Enabled": true,
      "RuleExpressionType": "LambdaExpression",
      "WorkflowsToInject": null,
      "Rules": null,
      "LocalParams": null,
      "Expression": "true == true",
      "Actions": {
        "OnSuccess": {
          "Name": "OutputExpression",
          "Context": {
            "Expression": "10d"
          }
        },
        "OnFailure": null
      },
      "SuccessEvent": null
    },
    "IsSuccess": false,
    "ChildResults": null,
    "Inputs": {
      "Activity": {
        "ActivityId": 1,
        "Description": "Activity Description",
        "Quote": null
      }
    },
    "ActionResult": {
      "Output": null,
      "Exception": null
    },
    "ExceptionMessage": "Error while compiling rule `Other unrelated rule`: No applicable method 'Sum' exists in type 'Object', in ScopedParam: PiecesCount"
  }
]

Any help / workaround will be much appreciated!

Thanks in advance!

Best regards!

Bugattisport1 commented 1 year ago

I had the same issue. Although it is not an ideal solution, I had success with doing some data massaging before executing the expression. Specifically, replacing all "null" values in the object with a string "null", then in my expression I had some logic like this: Int(record) > (recordThreshold== "null" ? (null) : Int(recordThreshold)) This allowed me to do comparisons with data that has a possibility of having a null value. I think this is more of a Dynamic LINQ issue rather than a rule engine issue.

abbasc52 commented 1 year ago

@ghigad Thanks for bringing this up. Let me share some details regarding RulesEngine which might help and this is based on the type of input you pass: 1) Pass a dynamic Object(aka ExpandoObject) - Here RulesEngine tries to convert the input to an approx typed object and may fail at compile if certain values are changing types (or null) 2) Pass a strong typed object - This will handle all scenarios perfectly as RulesEngine does not need to guess 3) Pass a JObject from Newtonsoft.Json - In this case, RulesEngine will support dynamic references to even not present properties, you would need to handle null checks at runtime. But it will require typecasting if you want to use any C# methods like sum.

There is another option to use CustomTypes and add a Utility class to handle such scenarios and keep the rules clean.

Let me know if this helps.

ghigad commented 1 year ago

Hi @Bugattisport1 ,

Thanks for your feedback.

Unfortunately, I'm not sure this will help since the expression seems to fail as a whole, no matter if it is reached by the ternary operator...

Unless I didn't quite understand you suggestion.

Maybe an example could help me understand?

Best regard!

ghigad commented 1 year ago

Hi @abbasc52,

Thank you for your feedback.

  1. I used to use an ExpandoObject because I was doing some processing on my data before passing it to the rules engine.
    • I was creating a new type with new properties depending on the input object.
    • I first started experiencing my problem in this setup.
    • This now makes sense since you confirmed that this is a normal behavior for ExpandoObjects.
  2. I completely removed the process that was generating the ExpandoObject and now pass a strongly typed object
    • I still face the issue, even with my strongly typed object
    • I even tried passing all the types involved in the object hierarchy in ReSettings,CustomTypes, but it still didn't work
  3. I didn't try converting my object to a JObject object with NewtonSoft.Json, but I will definitely give it a try.
    • I will keep you posted with the results

I also tried to create a utility class and pass it to the CustomTypes, but with very little success...

Any other ideas?

Best regards!

ghigad commented 1 year ago

Hi @abbasc52,

A quick follow-up...

I went through my tests again and found out that my test program was using an ExpandoObject.

I updated my test to use a strongly typed object and my basic rules worked!

I'll rerun through my monster rules and make sure they use strongly typed objects and see if I still face the issue.

I'll keep you posted of my results.

Thank you again for your help!

Best regards!

ghigad commented 1 year ago

Hi @abbasc52,

Sorry for the delay...

Using strongly typed objects solved my issue!

Thank you for your help!

Best regards!