Invoking lambda expressions in Expression trees

2019-09-01 07:25发布

问题:

I have a SelectionCriteria class that I use for building Entity Framework query expressions, based on PredicateBuilder. Within its limits, it's working fine. I'd like to extend it so that it can query whether a field contains a substring. My problem is that I can't see how to build the needed expression object.

My actual class supports and, or, and not, but they aren't relevant to my question. So I've simplified my example code to handle only a single binary operation:

public class SelectionCriteria
{
    public SelectionComparison selectionComparison { get; set; }
    public string fieldName { get; set; }
    public object fieldValue { get; set; }

    public Expression<Func<T, bool>> constructSinglePredicate<T>()
    {
        var type = typeof(T);

        if (type.GetProperty(this.fieldName) == null && type.GetField(this.fieldName) == null)
            throw new MissingMemberException(type.Name, this.fieldName);

        ExpressionType operation;
        if (!operationMap.TryGetValue(this.selectionComparison, out operation))
            throw new ArgumentOutOfRangeException("selectionComparison", this.selectionComparison, "Invalid filter operation");

        var parameter = Expression.Parameter(type);
        var member = Expression.PropertyOrField(parameter, this.fieldName);
        var value = (this.fieldValue == null) ? Expression.Constant(null) : Expression.Constant(this.fieldValue, this.fieldValue.GetType());

        try
        {
            var converted = (value.Type != member.Type)
                ? (Expression) Expression.Convert(value, member.Type)
                : (Expression) value;

            var comparison = Expression.MakeBinary(operation, member, converted);

            var lambda = Expression.Lambda<Func<T, bool>>(comparison, parameter);

            return lambda;
        }
        catch (Exception)
        {
            throw new InvalidOperationException(
                String.Format("Cannot convert value \"{0}\" of type \"{1}\" to field \"{2}\" of type \"{3}\"", this.fieldValue,
                    value.Type, this.fieldName, member.Type));
        }
    }

    private static Dictionary<SelectionComparison, ExpressionType> operationMap =
        new Dictionary<SelectionComparison, ExpressionType>
        {
            { SelectionComparison.Equal, ExpressionType.Equal },
            { SelectionComparison.GreaterThan, ExpressionType.GreaterThan },
        };
}

public enum SelectionComparison
{
    Equal,
    GreaterThan,
    Contains,
};

Usage is simple:

var criteria = new SelectionCriteria
{
    selectionComparison = SelectionComparison.GreaterThan,
    fieldName = "worktobegindate",
    fieldValue = DateTime.Now.AddDays(-2)
};

var predicate = criteria .constructPredicate<job>();
var jobs = myDbContext.jobs.Where(predicate);

So, my problem - I need a method, like my constructSinglePredictate() above, that returns an Expression> and applies a .Contains(

It's trivial to apply Contains() to a string, given a string to compare it to, but I'm having difficulty figuring out how to do the same in an Expression.

Ideas?

回答1:

As is usual, I was thinking about things incorrectly. I don't need a lambda that calls a method on a string, I need a MethodExpression (here extracted from the methodMap dictionary):

public Expression<Func<T, bool>> constructMethodCallPredicate<T>()
{
    var type = typeof(T);

    if (type.GetProperty(this.fieldName) == null && type.GetField(this.fieldName) == null)
        throw new MissingMemberException(type.Name, this.fieldName);

    MethodInfo method;
    if (!methodMap.TryGetValue(this.selectionComparison, out method))
        throw new ArgumentOutOfRangeException("selectionComparison", this.selectionComparison, "Invalid filter operation");

    var parameter = Expression.Parameter(type);
    var member = Expression.PropertyOrField(parameter, this.fieldName);
    var value = (this.fieldValue == null) ? Expression.Constant(null) : Expression.Constant(this.fieldValue, this.fieldValue.GetType());

    try
    {
        var converted = (value.Type != member.Type)
            ? (Expression)Expression.Convert(value, member.Type)
            : (Expression)value;

        var methodExpression = Expression.Call(member, method, converted);

        var lambda = Expression.Lambda<Func<T, bool>>(methodExpression, parameter);

        return lambda;
    }
    catch (Exception)
    {
        throw new InvalidOperationException(
            String.Format("Cannot convert value \"{0}\" of type \"{1}\" to field \"{2}\" of type \"{3}\"", this.fieldValue,
                value.Type, this.fieldName, member.Type));
    }
}

private static readonly Dictionary<SelectionComparison, MethodInfo> methodMap =
    new Dictionary<SelectionComparison, MethodInfo>
    {
        { SelectionComparison.Contains, typeof(string).GetMethod("Contains", new[] { typeof(string) }) },
        { SelectionComparison.StartsWith, typeof(string).GetMethod("StartsWith", new[] { typeof(string) }) },
        { SelectionComparison.EndsWith, typeof(string).GetMethod("EndsWith", new[] { typeof(string) }) },
    };

public enum SelectionComparison
{
    Equal,
    NotEqual,
    LessThan,
    LessThanOrEqual,
    GreaterThan,
    GreaterThanOrEqual,
    Contains,
    StartsWith,
    EndsWith,
};

Getting an actual "Like" comparison to work, using SqlFunctions.PatIndex is a bit more complicated, PatIndex() returns an int, and I need to wrap it in a >0 expression, but it works just fine, as well.