I\'m in the process of creating a more elaborate filtering system for this huge project of ours. One of the main predicates is being able to pass comparations through a string p
One of the at-first-glance-magical features of the C# compiler can do the hard work for you. You probably know you can do this:
Func<decimal, bool> totalCostIsUnder50 = d => d < 50m;
that is, use a lambda expression to assign a Func
. But did you know you can also do this:
Expression<Func<decimal, bool>> totalCostIsUnder50Expression = d => d < 50m;
that is, use a lambda expression to assign an Expression
that expresses a Func
? It's pretty neat.
Given you say
The comparison builder is not the issue, that's the easy bit. The hard part is actually returning the expression
I'm assuming you can fill in the blanks here; suppose we pass in `"<50" to:
Expression<Func<decimal, bool>> TotalCostCheckerBuilder(string criterion)
{
// Split criterion into operator and value
// when operator is < do this:
return d => d < value;
// when operator is > do this:
return d => d > value;
// and so on
}
Finally, to compose your Expression
s together with &&
(and still have an Expression
), do this:
var andExpression = Expression.And(firstExpression, secondExpression);
The hard part is actually returning the expression.
Translate strings into more structured constructions like enums and classes to define properties, operators and filters:
Enum Parameter
TotalCost
Required
End Enum
Enum Comparator
Less
More
Equals
End Enum
Class Criterion
Public ReadOnly Parameter As Parameter
Public ReadOnly Comparator As Comparator
Public ReadOnly Value As Double
Public Sub New(Parameter As Parameter, Comparator As Comparator, Value As Double)
Me.Parameter = Parameter
Me.Comparator = Comparator
Me.Value = Value
End Sub
End Class
Then a function to create expression is defined:
Function CreateExpression(Criteria As IEnumerable(Of Criterion)) As Expression(Of Func(Of Field, Boolean))
Dim FullExpression = PredicateBuilder.True(Of Field)()
For Each Criterion In Criteria
Dim Value = Criterion.Value
Dim TotalCostExpressions As New Dictionary(Of Comparator, Expression(Of Func(Of Field, Boolean))) From {
{Comparator.Less, Function(Field) Field.TotalCost < Value},
{Comparator.More, Function(Field) Field.TotalCost > Value},
{Comparator.Equals, Function(Field) Field.TotalCost = Value}
}
Dim RequiredExpressions As New Dictionary(Of Comparator, Expression(Of Func(Of Field, Boolean))) From {
{Comparator.Less, Function(Field) Field.Required < Value},
{Comparator.More, Function(Field) Field.Required > Value},
{Comparator.Equals, Function(Field) Field.Required = Value}
}
Dim Expressions As New Dictionary(Of Parameter, IDictionary(Of Comparator, Expression(Of Func(Of Field, Boolean)))) From {
{Parameter.TotalCost, TotalCostExpressions},
{Parameter.Required, RequiredExpressions}}
Dim Expression = Expressions(Criterion.Parameter)(Criterion.Comparator)
FullExpression = Expression.And(Expression)
Next
Return FullExpression
End Function
PredicateBuilder
taken here is needed to combine two expressions with AND
operator.
Usage:
Function Usage() As Integer
Dim Criteria = {
New Criterion(Parameter.TotalCost, Comparator.Less, 50),
New Criterion(Parameter.Required, Comparator.More, 5),
New Criterion(Parameter.Required, Comparator.Less, 10)}
Dim Expression = CreateExpression(Criteria)
End Function
It will create expression exactly like provided in an example
field => field.TotalCost < 50 && field.Required > 5 && field.Required < 10
To generate expression, that would be translated to SQL (eSQL) you should generate Expression
manually. Here is example for GreaterThan filter creating, other filters can be made with similar technique.
static Expression<Func<T, bool>> CreateGreaterThanExpression<T>(Expression<Func<T, decimal>> fieldExtractor, decimal value)
{
var xPar = Expression.Parameter(typeof(T), "x");
var x = new ParameterRebinder(xPar);
var getter = (MemberExpression)x.Visit(fieldExtractor.Body);
var resultBody = Expression.GreaterThan(getter, Expression.Constant(value, typeof(decimal)));
return Expression.Lambda<Func<T, bool>>(resultBody, xPar);
}
private sealed class ParameterRebinder : ExpressionVisitor
{
private readonly ParameterExpression _parameter;
public ParameterRebinder(ParameterExpression parameter)
{ this._parameter = parameter; }
protected override Expression VisitParameter(ParameterExpression p)
{ return base.VisitParameter(this._parameter); }
}
Here is the example of usage. (Assume, that we have StackEntites
EF context with entity set TestEnitities of TestEntity
entities)
static void Main(string[] args)
{
using (var ents = new StackEntities())
{
var filter = CreateGreaterThanExpression<TestEnitity>(x => x.SortProperty, 3);
var items = ents.TestEnitities.Where(filter).ToArray();
}
}
Update:
For your creation of complex expression you may use code like this:
(Assume have already made CreateLessThanExpression
and CreateBetweenExpression
functions)
static Expression<Func<T, bool>> CreateFilterFromString<T>(Expression<Func<T, decimal>> fieldExtractor, string text)
{
var greaterOrLessRegex = new Regex(@"^\s*(?<sign>\>|\<)\s*(?<number>\d+(\.\d+){0,1})\s*$");
var match = greaterOrLessRegex.Match(text);
if (match.Success)
{
var number = decimal.Parse(match.Result("${number}"));
var sign = match.Result("${sign}");
switch (sign)
{
case ">":
return CreateGreaterThanExpression(fieldExtractor, number);
case "<":
return CreateLessThanExpression(fieldExtractor, number);
default:
throw new Exception("Bad Sign!");
}
}
var betweenRegex = new Regex(@"^\s*(?<number1>\d+(\.\d+){0,1})\s*-\s*(?<number2>\d+(\.\d+){0,1})\s*$");
match = betweenRegex.Match(text);
if (match.Success)
{
var number1 = decimal.Parse(match.Result("${number1}"));
var number2 = decimal.Parse(match.Result("${number2}"));
return CreateBetweenExpression(fieldExtractor, number1, number2);
}
throw new Exception("Bad filter Format!");
}