Search code examples
c#asp.net-mvclinqdynamic-linq

Dynamic LINQ Any() is Returning Invalid Results


Problem Setup

I'm attempting to implement validation rules into my app using the System.Linq.Dynamic.Core package, but I'm running into weird behavior. My idea is to have the ability to create and manage these validation rules from within the app itself. So, I have a ValidationRule object in my database defined as:

public class ValidationRule {
    public string Description { get; set; }
    public string ErrorMessage { get; set; }
    public short Id { get; set; }
    public bool IsActive { get; set; }
    public string Name { get; set; }
    public string NewExpression { get; set; }
    public string OldExpression { get; set; }
}

The NewExpression and OldExpression properties contain the expressions I want to pass to the dynamic version of Any(). The only important parts of the ValidationRule object are the Name, ErrorMessage, NewExpression, and OldExpression, so I project them into a DTO:

public class ValidationRuleDto {
    public string ErrorMessage { get; set; }
    public string Name { get; set; }
    public string NewExpression { get; set; }
    public string OldExpression { get; set; }
}

This DTO is then passed to a ValidationRuleHandler to evaluate an old and a new instance of the object being validated with the expressions from the DTO. The ValidationRuleHandler looks like this:

public static class ValidationRuleHandler {
    private static readonly ParsingConfig _parsingConfig = new() {
        AreContextKeywordsEnabled = false
    };

    public static ICollection<string> Validate(
        dynamic oldObject,
        dynamic newObject,
        IEnumerable<ValidationRuleDto> rules) => rules.Select(
        _ => {
            var oldResult = new[] {
                oldObject
            }.AsQueryable().Any(_parsingConfig, _.OldExpression);
            var newResult = new[] {
                newObject
            }.AsQueryable().Any(_parsingConfig, _.NewExpression);

            Debug.WriteLine($"Old => {oldObject.StageText} => {_.OldExpression} => {oldResult}");
            Debug.WriteLine($"New => {newObject.StageText} => {_.NewExpression} => {newResult}");
            
            return !(oldResult && newResult)
                ? null
                : $"[{_.Name}] {_.ErrorMessage}";
        }).Where(
        _ => _ != null).ToHashSet();
}

For the old and new objects being passed to Validate() I project the object into an old snapshot (from the database as it is) and new snapshot (from the database but with the changes applied) DTOs. In the Validate() I add each of the objects to a single item collection and convert them to an IQueryable so I can use the dynamic Any(). My thought process here is that the expressions for the rule just need to evaluate to a true/false result, and since Any() returns true/false if the expression's conditions passed, it seemed the most appropriate way to go.

Problem

The problem that I'm running into is that the results I expect are not happening when running the app. For reference, the app is an ASP.NET MVC 5 (5.2.9) app targetting .NET Framework 4.8. However, when using LINQPad (5.46.00) to test the ValidationRuleHandler the results are correct. For example here's the output of the Debug statements from the app when it's processing three validation rules that apply to the user:

  • Old => Not Sold => (StageText != "Closed") => True
  • New => Closed => (StageText == "Closed") => False WRONG
  • Old => Not Sold => (StageText != "Not Sold") => True WRONG
  • New => Closed => (StageText == "Not Sold") and (ReasonNotSoldText == null) => False
  • Old => Not Sold => (StageText != "Ready to Invoice") => True
  • New => Closed => (StageText == "Ready to Invoice") and (PricingMethodText == null) => False

And here's the LINQPad result for the exact same validation rules and snapshot objects (exact same values):

  • Old => Not Sold => (StageText != "Closed") => True
  • New => Closed => (StageText == "Closed") => True
  • Old => Not Sold => (StageText != "Not Sold") => False
  • New => Closed => (StageText == "Not Sold") and (ReasonNotSoldText == null) => False
  • Old => Not Sold => (StageText != "Ready to Invoice") => True
  • New => Closed => (StageText == "Ready to Invoice") and (PricingMethodText == null) => False

As you can see, when ValidationRuleHandler is called from LINQPad it evaluates the expressions correctly, but when processed from the app, it's wrong.

I can't figure out why it's behaving this way. Thinking through it, I can't see a problem, and LINQPad behaves as expected, but the app is not and I don't know what else to do. If anyone has any suggestions, I'd appreciate them. I'm also open to alternative suggestions to accomplish the same goal if anyone has had to deal with something similar before.


Solution

  • So, after many hours of trying different solutions to see where exactly it was failing, nothing worked, until I changed the expression to use Equals(). Instantly worked after that, and I'm not sure why this: (StageText.Equals("Closed")); works and this: (StageText == "Closed"); doesn't.

    I'm still baffled why the original way worked when I tested it in LINQPad but not in the app. @Stef also confirmed it works, so I'm confused. Maybe it has something to do with the snapshot values being pulled from the database, or the validation rule expressions also being pulled from the database?

    Anyway, it works with Equals() so I'm rolling with it.