表达式树中的可空比较

本文关键字:比较 表达式 | 更新日期: 2023-09-27 18:05:42

我试图建立一个动态的LINQ查询,在实体框架中,从用户提供的一组收集标准。最终,这将包括更复杂的行为,但目前我只有一个字段名称和值的列表,并且我想返回字段名称具有这些值的所有记录。

我的基本结构是:

public IEnumerable<ThingViewModel> getMythings(SelectionCriteria selectionCriteria)
{
    var predicate = constructPredicate<Thing>(selectionCriteria);
    var things = this.dbContext.Things.Where(predicate).ToList();
    return Mapper.Map<List<Thing>, List<ThingViewModel>>(things);
}

所有有趣的工作都在constructPredicate()中:

private static Expression<Func<T, bool>> constructPredicate<T>(SelectionCriteria selectionCriteria)
{
    // using Pete Montgomery's PredicateBuilder:
    // http://petemontgomery.wordpress.com/2011/02/10/a-universal-predicatebuilder/
    var predicate = PredicateBuilder.True<T>();
    foreach (var item in selectionCriteria.andList)
    {
        // Accessing foreach values in closures can result in unexpected results.
        // http://stackoverflow.com/questions/14907987/access-to-foreach-variable-in-closure
        var fieldName = item.fieldName;
        var fieldValue = item.fieldValue;
        var parameter = Expression.Parameter(typeof (T), "t");
        var property = Expression.Property(parameter, fieldName);
        var value = Expression.Constant(fieldValue);
        var lambda = buildCompareLambda<T>(property, value, parameter);
        predicate = predicate.And(lambda);
    }
    return predicate;
}

所有这一切似乎是一个完全合理的结构,但它是在buildCompareLambda(),我有困难。我没有看到通用的方法,我需要为不同的类型创建不同的方法。我从处理字符串开始,这很简单。接下来,我尝试处理整数,但结果发现数据库中的整数字段是可空的,这引入了一个全新的复杂性类。

我的buildCompareLambda(),到目前为止:

private static Expression<Func<T, bool>> buildCompareLambda<T>(
    MemberExpression property,
    ConstantExpression value,
    ParameterExpression parameter)
{
    Expression<Func<T, bool>> lambda = null;
    if (property.Type == typeof (string))
        lambda = buildStringCompareLambda<T>(property, value, parameter);
    else if (property.Type.IsGenericType && Nullable.GetUnderlyingType(property.Type) != null)
        lambda = buildNullableCompareLambda<T>(property, value, parameter);
    if (lambda == null)
        throw new Exception(String.Format("SelectrionCriteria cannot handle property type '{0}'", property.Type.Name));
    return lambda;
}

就像我说的,buildStringCompareLambda很简单:

private static Expression<Func<T, bool>> buildStringCompareLambda<T>(
    MemberExpression property,
    ConstantExpression value,
    ParameterExpression parameter)
{
    var equalsMethod = typeof (string).GetMethod("Equals", 
            new[] {typeof (string), typeof (string)});
    var comparison = Expression.Call(equalsMethod, property, value);
    return Expression.Lambda<Func<T, bool>>(comparison, parameter);
}

但是buildNullableCompareLambda()变得很难看:

private static Expression<Func<T, bool>> buildNullableCompareLambda<T>(MemberExpression property,
    ConstantExpression value,
    ParameterExpression parameter)
{
    var underlyingType = Nullable.GetUnderlyingType(property.Type);
    if (underlyingType == typeof (int) || underlyingType == typeof (Int16) || underlyingType == typeof (Int32) ||
        underlyingType == typeof (Int64) || underlyingType == typeof (UInt16) || underlyingType == typeof (UInt32) ||
        underlyingType == typeof (UInt64))
    {
        var equalsMethod = underlyingType.GetMethod("Equals", new[] {underlyingType});
        var left = Expression.Convert(property, underlyingType);
        var right = Expression.Convert(value, underlyingType);
        var comparison = Expression.Call(left, equalsMethod, new Expression[] {right});
        return Expression.Lambda<Func<T, bool>>(comparison, parameter);
    }
    return null;
}

我打算在buildNullableCompareLambda()中添加对更多类型的支持,并将每种类型的处理移动到函数中,以便可以从buildCompareLambda()和buildNullableCompareLambda()调用相同的代码。但那是以后的事了——目前我还在比较int型。目前,我正在将属性和值都转换为底层类型,因为我不想为每个整数类型都有单独的函数,也不希望用户必须关心EF将字段建模为Int16还是Int32。对于非空字段,这是有效的

我一直在浏览SO,并找到一些答案,这就是我如何得到的,但是我所看到的关于处理可空类型的答案都不适合我,因为它们并不真正处理null。

在我的例子中,如果用户传递给我一个选择条件,其中的项应该等于null,我希望返回该字段为null的记录,并且关于将两边转换为基本类型的这一点似乎不起作用。我得到一个"对象引用未设置为对象的实例"异常。

在SQL中,我想要的是一个"WHERE field is NULL",如果值为NULL,或者"WHERE field = 'value'",如果不是。我不知道如何将这种替代方法构建到表达式树中。

任何想法?

添加:有人建议我使用Expression.Equal().

这样,我的循环变成了:

private static Expression<Func<T, bool>> constructPredicate<T>(SelectionCriteria selectionCriteria)
{
    var predicate = PredicateBuilderEx.True<T>();
    var foo = PredicateBuilder.True<T>();
    foreach (var item in selectionCriteria.andList)
    {
        var fieldName = item.fieldName;
        var fieldValue = item.fieldValue;
        var parameter = Expression.Parameter(typeof (T), "t");
        var property = Expression.Property(parameter, fieldName);
        var value = Expression.Constant(fieldValue);
        var comparison = Expression.Equal(property, value);
        var lambda = Expression.Lambda<Func<T, bool>>(comparison, parameter);
        predicate = PredicateBuilderEx.And(predicate, lambda);
    }
    return predicate;
}

这行不通。我得到一个异常:

没有为这些类型定义二进制操作符EqualSystem.Nullable 1(系统。

表达式树中的可空比较

就像经常发生的情况一样,这里的人可能不能完全给出答案,但他们得到了大部分的答案,并且足够接近,我可以算出剩下的。

表达式。Equal要求两个形参的类型相同。如果其中一个是可空的,那么它们都需要是可空的。但这并不难处理:

private static Expression<Func<T, bool>> constructPredicate<T>(SelectionCriteria selectionCriteria)
{
    var predicate = PredicateBuilderEx.True<T>();
    var foo = PredicateBuilder.True<T>();
    foreach (var item in selectionCriteria.andList)
    {
        var fieldName = item.fieldName;
        var fieldValue = item.fieldValue;
        var parameter = Expression.Parameter(typeof (T), "t");
        var property = Expression.Property(parameter, fieldName);
        var value = Expression.Constant(fieldValue);
        var converted = Expression.Convert(value, property.Type);
        var comparison = Expression.Equal(property, converted);
        var lambda = Expression.Lambda<Func<T, bool>>(comparison, parameter);
        predicate = PredicateBuilderEx.And(predicate, lambda);
    }
    return predicate;
}

谢谢。

正如Lee在他的评论中所说,您不需要为每种类型实现buildNullableCompareLambda<T>。已经有一个方法可以检查左表达式和右表达式的类型,如果它们是用户定义的类型,则调用Equals方法,如果它们是可空类型,则进行提升和适当的比较。看到这里。

你的lambda基本上是:

var property = Expression.Property(parameter, fieldName);
var value = Expression.Constant(fieldValue);
var lambda = Expression.Equal(property, value);
编辑:

在我看来这是一个bug。Eric Lippert是这么认为的(链接)。文档说明了它们都是相同类型的场景,以及在这种情况下该怎么做:

如果离开了。打字和右转。类型都是非空的,节点则不是解除。节点类型为布尔型。如果离开了。打字和右转。类型都为空,则节点被解除。节点类型为布尔。

如果一个是可空的而另一个是不可空的,它并没有确切地说明会发生什么。在引用的同一链接中,Eric给出了一个解决方案: