Reputation: 2065
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
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
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