我正在尝试使用用户提供的一组收集条件在Entity Framework中构建动态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()调用相同的代码。但这是为了未来 - 目前我仍然坚持比较整数。目前,我正在将属性和值转换为基础类型,因为我不希望为每个整数类型分别具有单独的函数,并且我不希望用户必须关心EF是否为某个字段建模进入Int16或Int32。对于非空字段,这是有效的。
我一直在浏览SO,并找到一些答案,这就是我已经得到的尽可能多的答案,但我在处理可空类型时看到的答案都没有真正对我有用,因为他们没有真的处理空值。
在我的情况下,如果用户向我传递了一个选项标准,其中一个项应该等于null,我想返回该字段为null的记录,以及关于将两边转换为基本类型的这一位似乎不起作用。我得到一个“对象引用未设置为对象的实例”异常。
在SQL中,我想要的是“WHERE字段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;
}
这不起作用。我得到一个例外:
没有为类型定义二元运算符Equal 'System.Nullable`1 [System.Int16]'和'System.Int16'。
答案 0 :(得分:9)
通常情况下,这里的人可能不会得到答案,但他们大部分都是这样,并且足够接近我可以解决剩下的问题。
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 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;
}
谢谢,所有。
答案 1 :(得分:1)
正如Lee在评论中所述,您不需要为每种类型实施buildNullableCompareLambda<T>
。已经有一种方法可以检查左表达式和右表达式的类型,如果它们是用户定义的类型,则调用Equals
方法,如果它们是可空类型,则进行提升和正确比较。见here.
你的lambda基本上是:
var property = Expression.Property(parameter, fieldName);
var value = Expression.Constant(fieldValue);
var lambda = Expression.Equal(property, value);
编辑:
在我看来,这是一个错误。 Eric Lippert这么认为(link)。文档说明了它们属于同一类型以及该怎么做的场景:
如果left.Type和right.Type都是非可空的,则节点不是 解除。节点的类型是布尔值。如果left.Type和right.Type 都可以为空,节点被提升。节点的类型是 布尔值。
如果一个可以为空并且另一个不可以,那么它究竟会发生什么。在引用的相同链接中,Eric提供了一种解决方法。