Tiago B
Tiago B

Reputation: 2065

Dynamic where operator c#

This question was asked 12 years ago, but since dotnet has changed so much, I wonder if there is a solution for it nowadays.

I have a model that represents the rule and operator that I want to perform:

public class RuleStatement
{
    public string Path { get; set; }
    public string Operator { get; set; }
    public int Value { get; set; }
}

And the source:

public class Foo
{
   public string Path {get;set;}
   public int Value {get;set;}
}

I want to perform this dynamic where operator on the source, so only the ones that match with all the rules will be returned. Example:

List<Foo> source = new List<Foo>();
//Sample of source
source.Add(new Foo{Path="A", Value = 10});
source.Add(new Foo{Path="B", Value = 20});
source.Add(new Foo{Path="C", Value = 30});

//Scenario 1 => Expected to be true
List<RuleStatement> rules = new List<RuleStatement>();
rules.Add(new RuleStatement{Path="A", Operator = ">=", Value=10});
rules.Add(new RuleStatement{Path="B", Operator = "==", Value=20});

bool isMatch = rules.All(rule => 
   source.Any(s => s.Path == rule.Path && s.Value $"{rule.Operator}" rule.Value));

//Scenario 2 => Expected to be false
List<RuleStatement> rules = new List<RuleStatement>();
rules.Add(new RuleStatement{Path="A", Operator = ">=", Value=100});
rules.Add(new RuleStatement{Path="B", Operator = "==", Value=20});

bool isMatch = rules.All(rule => 
   source.Any(s => s.Path == rule.Path && s.Value $"{rule.Operator}" rule.Value));

I couldn't find any working solution for this, so is it possible to perform this dynamic where operation on .NET Core 5?

If it isn't possible, what could be a workaround to solve this problem?

Upvotes: 0

Views: 276

Answers (2)

Jimi
Jimi

Reputation: 32233

Problem:
A collection of class elements, defined as:

List<Foo> source = new List<Foo>();

public class Foo
{
   public string Path {get;set;}
   public int Value {get;set;}
}

needs to be compared to a set of rules defined in another class object, defined as:

public class RuleStatement
{
    public string Path { get; set; }
    public string Operator { get; set; }
    public int Value { get; set; }
}

To comply with a Rule specified by a RuleStatement object, a Foo element must match the Path property of a RuleStatement and a comparison, applied to the Value property and based on the Operator property, must return a positive result.

The Operator property is defined as a string, which makes an implementation based on implicit/explicit operators somewhat convoluted.
The test of the compliance of Foo elements to the specified rules is also time-sensitive.


A possible solution is to map the Operator property values to a Func delegate that performs the comparison based on the operator and, possibly, a set of other conditions specific to each operator.

A Dictionary is often used for this kind of task.
In the a simple form, it could be a Dictionary<string, Func<int, int, bool>>, since two int values are compared and a true/false result is expected.

The implementation could also be more generic, if the Foo and RuleStatement class can be modified/adapted. The Dictionary mapper could also be part of the RuleStatement class.

For example, make the Foo class implement an Interface that can be used in similar cases:

public interface IGenericFoos
{
    string Path { get; set; }
    int Value { get; set; }
}

public class Foo : IGenericFoos
{
    public string Path { get; set; }
    public int Value { get; set; }
}

The RuleStatement class can be changed in:

public class RuleStatement<T> where T : IGenericFoos
{
    public static Dictionary<string, Func<T, RuleStatement<T>, bool>> operators =
        new Dictionary<string, Func<T, RuleStatement<T>, bool>>() {
            [">="] = (T, R) => T.Value >= R.Value,
            ["<="] = (T, R) => T.Value <= R.Value,
            ["<>"] = (T, R) => T.Value != R.Value,
            ["!="] = (T, R) => T.Value != R.Value,
            ["=="] = (T, R) => T.Value == R.Value,
            ["="] = (T, R) => T.Value == R.Value,
            ["<"] = (T, R) => T.Value < R.Value,
            [">"] = (T, R) => T.Value > R.Value,
        };

    public string Path { get; set; }
    public string Operator { get; set; }
    public int Value { get; set; }

    public bool Eval(T ifoo) => ifoo.Path == Path && operators[Operator](ifoo, this);
}

A LINQ query that evaluates the compliance of all Foo objects to a set of rules can then be simplified to:

var source = new List<Foo> { ... }
var rules = new List<RuleStatement<Foo>> { ... }
// [...]
bool isMatch = rules.All(rule => source.Any(s => rule.Eval(s)));

// Or implicitly:
bool isMatch = rules.All(rule => source.Any(rule.Eval));

Other types of operators can be added to perform different comparison or other operations. E.g., "+" and "-" operators can be evaluated using a fixed value, e.g:

["+"] = (T, R) => T.Value + R.Value >= 0,
["-"] = (T, R) => T.Value - R.Value >= 0,

or using variable value, adding a Property (also specific overloaded Constructors, eventually) to the RuleStatement<T> class.
This kind of implementation gives more flexibility if more complex comparisons/operations may become necessary in the future.


If these classes cannot be modified, the Dictionary can be used as a stand-alone Field (or whatever fits) and the LINQ query can be changed (keeping the original definition shown in the OP) in:

bool isMatch = rules.All(rule => source
    .Any(s => s.Path == rule.Path && operators[rule.Operator](s.Value, rule.Value)));

Upvotes: 1

LionAM
LionAM

Reputation: 1401

You learn something new every day. If you actually want to go with strings and code as few as possible (and perfomance doesn't matter), you could use the NuGet package Microsoft.CodeAnalysis.Scripting:

using Microsoft.CodeAnalysis.CSharp.Scripting;

s.Path == rule.Path && CSharpScript.EvaluateAsync<bool>($"{s.Value} {rule.Operator} {rule.Value}").Result;

If performance is more important, then maybe this will be the way to go:

This is already possible since .Net Framework 3.5, but not directly with strings. But you could write a function to achieve this. For your case, you will need a switch case for the operator - and maybe it makes sense to cache the delegates.

using System.Linq.Expressions;

var param1 = Expression.Parameter(typeof(int));
var param2 = Expression.Parameter(typeof(int));
var expr = Expression.LessThan(param1, param2);

var func = Expression.Lambda<Func<int, int, bool>>(expr, param1, param2).Compile();

var result = func(1,2);

Upvotes: 2

Related Questions