表达式树中的可空比较
本文关键字:比较 表达式 | 更新日期: 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给出了一个解决方案: