【问题标题】:Trying to filter on a Nullable type using Expression Trees尝试使用表达式树过滤 Nullable 类型
【发布时间】:2013-01-31 23:57:58
【问题描述】:

我在下面粘贴了我的整个测试应用程序。它相当紧凑,所以我希望这不是问题。您应该能够简单地将其剪切并粘贴到控制台应用程序中并运行它。

我需要能够过滤任何一个或多个 Person 对象的属性,直到运行时我才知道是哪一个。我知道这已经被到处讨论了,我已经研究过并且也在使用诸如PredicateBuilderDynamic Linq Library之类的工具,但是围绕它们的讨论往往更多地集中在排序和排序上,而且每个人都在苦苦挣扎面对 Nullable 类型时有自己的问题。所以我想我会尝试至少构建一个可以解决这些特定场景的补充过滤器。

在下面的示例中,我试图过滤掉在某个日期之后出生的家庭成员。关键是要过滤的对象上的 DateOfBirth 字段是 DateTime 属性。

我得到的最新错误是

在类型“System.String”和“System.Nullable`1[System.DateTime]”之间没有定义强制运算符。

这是问题所在。我尝试了几种不同的铸造和转换方法,但都失败了。最终,这将应用于 EF 数据库,该数据库也拒绝使用 DateTime.Parse(--) 等转换方法。

任何帮助将不胜感激!

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            List<Person> people = new List<Person>();
        people.Add(new Person { FirstName = "Bob", LastName = "Smith", DateOfBirth = DateTime.Parse("1969/01/21"), Weight=207 });
        people.Add(new Person { FirstName = "Lisa", LastName = "Smith", DateOfBirth = DateTime.Parse("1974/05/09") });
        people.Add(new Person { FirstName = "Jane", LastName = "Smith", DateOfBirth = DateTime.Parse("1999/05/09") });
        people.Add(new Person { FirstName = "Lori", LastName = "Jones", DateOfBirth = DateTime.Parse("2002/10/21") });
        people.Add(new Person { FirstName = "Patty", LastName = "Smith", DateOfBirth = DateTime.Parse("2012/03/11") });
        people.Add(new Person { FirstName = "George", LastName = "Smith", DateOfBirth = DateTime.Parse("2013/06/18"), Weight=6 });

            String filterField = "DateOfBirth";
            String filterOper = "<=";
            String filterValue = "2000/01/01";

            var oldFamily = ApplyFilter<Person>(filterField, filterOper, filterValue);

            var query = from p in people.AsQueryable().Where(oldFamily) 
                        select p;

            Console.ReadLine();
        }

        public static Expression<Func<T, bool>> ApplyFilter<T>(String filterField, String filterOper, String filterValue)
        {
            //
            // Get the property that we are attempting to filter on. If it does not exist then throw an exception
            System.Reflection.PropertyInfo prop = typeof(T).GetProperty(filterField);
            if (prop == null)
                throw new MissingMemberException(String.Format("{0} is not a member of {1}", filterField, typeof(T).ToString()));

            Expression convertExpression     = Expression.Convert(Expression.Constant(filterValue), prop.PropertyType);

            ParameterExpression parameter    = Expression.Parameter(prop.PropertyType, filterField);
            ParameterExpression[] parameters = new ParameterExpression[] { parameter };
            BinaryExpression body            = Expression.LessThanOrEqual(parameter, convertExpression);


            Expression<Func<T, bool>> predicate = Expression.Lambda<Func<T, bool>>(body, parameters);


            return predicate;

        }
    }

    public class Person
    {

        public string FirstName { get; set; }
        public string LastName { get; set; }
        public DateTime? DateOfBirth { get; set; }
        string Nickname { get; set; }
        public int? Weight { get; set; }

        public Person() { }
        public Person(string fName, string lName)
        {
            FirstName = fName;
            LastName = lName;
        }
    }
}

更新:2013/02/01

然后我的想法是将 Nullabe 类型转换为它的 Non-Nullable 类型版本。因此,在这种情况下,我们希望将 DateTime 转换为简单的 DateTime 类型。我在调用 Expression.Convert 调用之前添加了以下代码块,以确定和捕获 Nullable 值的类型。

//
//
Type propType = prop.PropertyType;
//
// If the property is nullable we need to create the expression using a NON-Nullable version of the type.
// We will get this by parsing the type from the FullName of the type 
if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
    String typeName = prop.PropertyType.FullName;
    Int32 startIdx  = typeName.IndexOf("[[") + 2;
    Int32 endIdx    = typeName.IndexOf(",", startIdx);
    String type     = typeName.Substring(startIdx, (endIdx-startIdx));
    propType        = Type.GetType(type);
}

Expression convertExpression = Expression.Convert(Expression.Constant(filterValue), propType);

这实际上可以从 DateTime 中删除 Nullable-ness,但会导致以下强制错误。我仍然对此感到困惑,因为我认为“Expression.Convert”方法的目的就是为了做到这一点。

在“System.String”和“System.DateTime”类型之间没有定义强制运算符。

继续我明确地将值解析为 DateTime 并将其插入到组合中......

DateTime dt = DateTime.Parse(filterValue);
Expression convertExpression = Expression.Convert(Expression.Constant(dt), propType);

...这导致了一个异常,超出了我对表达式、Lambda 及其相关同类知识的任何了解...

“System.DateTime”类型的参数表达式不能用于“ConsoleApplication1.Person”类型的委托参数

我不确定还有什么可以尝试的。

【问题讨论】:

    标签: linq c#-4.0 lambda expression-trees


    【解决方案1】:

    问题是在生成二进制表达式时,操作数必须是兼容的类型。如果不是,您需要对一个(或两个)执行转换,直到它们兼容为止。

    从技术上讲,您无法将DateTimeDateTime? 进行比较,编译器会隐式地将一个提升到另一个,这样我们就可以进行比较。由于编译器不是生成表达式的,我们需要自己执行转换。

    我已将您的示例调整为更通用(并且有效:D)。

    public static Expression<Func<TObject, bool>> ApplyFilter<TObject, TValue>(String filterField, FilterOperation filterOper, TValue filterValue)
    {
        var type = typeof(TObject);
        ExpressionType operation;
        if (type.GetProperty(filterField) == null && type.GetField(filterField) == null)
            throw new MissingMemberException(type.Name, filterField);
        if (!operationMap.TryGetValue(filterOper, out operation))
            throw new ArgumentOutOfRangeException("filterOper", filterOper, "Invalid filter operation");
    
        var parameter = Expression.Parameter(type);
    
        var fieldAccess = Expression.PropertyOrField(parameter, filterField);
        var value = Expression.Constant(filterValue, filterValue.GetType());
    
        // let's perform the conversion only if we really need it
        var converted = value.Type != fieldAccess.Type
            ? (Expression)Expression.Convert(value, fieldAccess.Type)
            : (Expression)value;
    
        var body = Expression.MakeBinary(operation, fieldAccess, converted);
    
        var expr = Expression.Lambda<Func<TObject, bool>>(body, parameter);
        return expr;
    }
    
    // to restrict the allowable range of operations
    public enum FilterOperation
    {
        Equal,
        NotEqual,
        LessThan,
        LessThanOrEqual,
        GreaterThan,
        GreaterThanOrEqual,
    }
    
    // we could have used reflection here instead since they have the same names
    static Dictionary<FilterOperation, ExpressionType> operationMap = new Dictionary<FilterOperation, ExpressionType>
    {
        { FilterOperation.Equal,                ExpressionType.Equal },
        { FilterOperation.NotEqual,             ExpressionType.NotEqual },
        { FilterOperation.LessThan,             ExpressionType.LessThan },
        { FilterOperation.LessThanOrEqual,      ExpressionType.LessThanOrEqual },
        { FilterOperation.GreaterThan,          ExpressionType.GreaterThan },
        { FilterOperation.GreaterThanOrEqual,   ExpressionType.GreaterThanOrEqual },
    };
    

    然后使用它:

    var filterField = "DateOfBirth";
    var filterOper = FilterOperation.LessThanOrEqual;
    var filterValue = DateTime.Parse("2000/01/01"); // note this is an actual DateTime object
    
    var oldFamily = ApplyFilter<Person>(filterField, filterOper, filterValue);
    
    var query = from p in people.AsQueryable().Where(oldFamily) 
                select p;
    

    我不知道这是否适用于所有情况,但它肯定适用于这种特殊情况。

    【讨论】:

    • 太棒了!我想这就是我要找的!不要利用,但是您可以将多次调用此方法的结果组合成一个表达式吗?例如:家庭中生日在 2001 年 1 月 1 日之后且名字是“Lori”的每个人?再次感谢!
    • 如果您只是在做一个简单的AND,那么您可以在查询中添加更多Where 子句。如果是OR,则必须将这些子句组合成一个表达式。在这种情况下,请查看PredicateBuilder 以使事情变得更容易。应该不会太复杂。
    • 谢谢...我确实找到了这篇以前的帖子stackoverflow.com/questions/1922497/… 用于简单的 AND 场景。
    • 如何将 TValue 传递给 ApplyFilter?
    【解决方案2】:

    如果您查询您的body 变量,您可以看到您正在创建的表达式的主体本质上是DateOfBirth &lt;= '2000/01/01'

    虽然从表面上看这似乎是正确的,但您正试图将该主体分配给一个采用 Person 的函数(这就是您的示例中的 T)并返回一个布尔值。您需要更改逻辑,使主体将输入反映为 Person 对象,访问 Person 实例上的 DateOfBirth 属性,然后执行比较。

    换句话说,您的表达式主体必须采用T,在其上找到正确的属性,然后进行比较。

    【讨论】:

    • 我确实想说这是一些很棒的信息,我会进一步研究!感谢您的回复!
    • @GaryO.Stenstrom:@JeffMercado 在他的回答中解决了这个问题。请参阅var fieldAccess = Expression.PropertyOrField(parameter, filterField); 行。这将创建我所说的 Person.DateOfBirth 属性访问。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多