Trying to filter on a Nullable type using Expressi

2019-04-02 08:20发布

问题:

I have pasted my entire test app below. It's fairly compact so I am hoping that it's not a problem. You should be able to simply cut and paste it into a console app and run it.

I need to be able to filter on any one or more of the Person objects' properties, and I don't know which one(s) until runtime. I know that this has beed discussed all over the place and I have looked into and am also using tools such as the PredicateBuilder & Dynamic Linq Library but the discussion aroung them tends to focus more on Sorting and ordering, and each have been struggling with their own issues when confronted with Nullable types. So I thought that I would attempt to build at least a supplemental filter that could address these particular scenarios.

In the example below I am trying to filter out the family members who were born after a certain date. The kick is that the DateOfBirth field on the objects being filterd is a DateTime property.

The latest error I am getting is

No coercion operator is defined between types 'System.String' and 'System.Nullable`1[System.DateTime]'.

Which is the problem. I have attempted several different means of casting and converting but to varying degrees of failure. Ultimately this will be applied against an EF database whcih has also balked at conversion methods such as DateTime.Parse(--).

Any assistence would be greatly appreciated!

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;
        }
    }
}

Update: 2013/02/01

My thought was then to convert the Nullabe type to it's Non-Nullable type version. So in this case we want to convert the <Nullable>DateTime to a simple DateTime type. I added the following code block before the call Expression.Convert call to determine and capture the type of the Nullable value.

//
//
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);

This actually worked in removing the Nullable-ness from the DateTime but resulted in the following Coercion error. I remain confused by this as I thought that the purpose of the "Expression.Convert" method was to do just this.

No coercion operator is defined between types 'System.String' and 'System.DateTime'.

Pushing on I explicitly parsed the value to a DateTime and plugged that into the mix ...

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

... which resulted in an exception that outpaces any knowledge I have of Expressions, Lambdas and their related ilk ...

ParameterExpression of type 'System.DateTime' cannot be used for delegate parameter of type 'ConsoleApplication1.Person'

I am not sure what's left to try.

回答1:

The problem is that when generating binary expressions, the operands have to be of compatible types. If not, you need to perform a conversion on one (or both) until they are compatible.

Technically, you cannot compare a DateTime with a DateTime?, the compiler implicitly promotes one to the other which allows us to do our comparisons. Since the compiler is not the one generating the expression, we need to perform the conversion ourself.

I've tweaked your example to be more general (and working :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 },
};

Then to use it:

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;

I don't know if this will work as-is for all cases but it certainly works for this particular case.



回答2:

If you interrogate your body variable, you can see that the body of the expression you're creating is essentially DateOfBirth <= '2000/01/01'.

While on the face this may seem correct, you are trying to assign that body to a function that takes a Person (that's what T would be in your example) and returns a bool. You need to alter your logic such that the body reflects the input as a Person object, accesses the DateOfBirth property on that instance of the Person, and then performs the comparison.

In other words, the body of your expression has to take a T, find the right property on it, and then compare.