Create predicate with nested classes with Expression
Asked Answered
C

2

6

I have this :

public class Company
{
    public int Id { get; set; }
    public string Name { get; set; }
}
public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int ZipCode { get; set; }
}
public class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int? Age { get; set; }
    public City City { get; set; }
    public Company Company { get; set; }
}

I'd like a some case generate the predicate like this :

var result = listPerson.Where(x => x.Age == 10).ToList<>();

Or this :

var result  = listPerson.Where( x => x.Company.Name == 1234).ToList();

Or this :

var result  = listPerson.Where( x => x.City.ZipCode == "MyZipCode").ToList();

Or this :

var result  = listPerson.Where( x => x.Company.Name == "MyCompanyName").ToList();

Then I created a "PredicateBuilder", that's work (I get the type, if nullable or not and I build the predicate) when I do this :

BuildPredicate<Person>("Age", 10); I get this : x => x.Age == 10

But I don't how manage when there is an nested property like this :

BuildPredicate<Person>("City.ZipCode", "MyZipCode"); 
I'd like get this : x => x.City.ZipCode == "MyZipCode"

Or this :

BuildPredicate<Person>("City.Name", "MyName"); 
I'd like get this : x => x.City.Name == "MyName"

Or this :

BuildPredicate<Person>("Company.Name", "MyCompanyName"); 
I'd like get this : x => x.Company.Name == "MyCompanyName"
Coenobite answered 10/1, 2013 at 13:20 Comment(4)
Are you completely sure you need to provide your property names like strings? Generally, that is bad idea.District
Please post existing code of BuildPredicateMatney
@District you are right, I'm open to change but there is UnitTest, then in case of typo ... but first I need to solve problemCoenobite
It's not about solving the issue. It's about self-perfection. It is good thing.District
G
9

(not intending to duplicate Jon - OP contacted me to provide an answer)

The following seems to work fine:

static Expression<Func<T,bool>> BuildPredicate<T>(string member, object value) {
    var p = Expression.Parameter(typeof(T));
    Expression body = p;
    foreach (var subMember in member.Split('.')) {
        body = Expression.PropertyOrField(body, subMember);
    }
    return Expression.Lambda<Func<T, bool>>(Expression.Equal(
        body, Expression.Constant(value, body.Type)), p);
}

The only functional difference between that and Jon's answer is that it handles null slightly better, by telling Expression.Constant what the expected type is. As a demonstration of usage:

static void Main() {
    var pred = BuildPredicate<Person>("City.Name", "MyCity");

    var people = new[] {
        new Person { City = new City { Name = "Somewhere Else"} },
        new Person { City = new City { Name = "MyCity"} },
    };
    var person = people.AsQueryable().Single(pred);
}
Griffiths answered 10/1, 2013 at 21:50 Comment(7)
Why your code (you and Jon) is all the time more simple than mine :)Coenobite
I implemented >, >=, < and <= when I the field is (int, decimal, ....) but I don't see how to implement "StartsWith" and "Contains"Coenobite
@Kris-I are we still having the same conversation? 'cos I think you jumped some context there.Griffiths
Yes and no. I use you BuilderPredicate and a I added a second one receiving in parameter a "TypeComparator", it's almost the same of yours but some code is different. This part "Expression.Equal( body, Expression.Constant(value, body.Type))" change depending of the comparator. I'd like to the same for "StartsWith", "Contains"Coenobite
@Kris-I I'll give you a hint: it involves something like Expression.Call(body, "StartsWith", null, targetVal)Griffiths
If the object is a string, how can you make the property value to be compared lower case without causing trouble for boolean, integer etc?Easterling
The same goes for datetime where you perhaps want to compare date only, not time.Easterling
D
5

You just need to split your expression by dots, and then iterate over it, using Expression.Property multiple times. Something like this:

string[] properties = path.Split('.');
var parameter = Expression.Parameter(typeof(T), "x");
var lhs = parameter;
foreach (var property in properties)
{
    lhs = Expression.Property(lhs, property);
}
// I've assumed that the target is a string, given the question. If that's
// not the case, look at Marc's answer.
var rhs  = Expression.Constant(targetValue, typeof(string));
var predicate = Expression.Equals(lhs, rhs);
var lambda = Expression.Lambda<Func<T, bool>>(predicate, parameter);
Dennet answered 10/1, 2013 at 13:23 Comment(3)
very minor feedback: Expression.Parameter requires a type, and it is worth providing a type to Expression.Constant, so that it can handle null correctly. The Exoression.Parameter snafu immediately tells me this is "notepad/markdown" code, though - so: close enough!Griffiths
@Kris-I the path.Split and foreach handles arbitrary levelsGriffiths
@MarcGravell: Oh completely. Will fix up.Dennet

© 2022 - 2024 — McMap. All rights reserved.