Home > OS >  Building Expression to search in child properies (collection) of object
Building Expression to search in child properies (collection) of object

Time:05-13

I have a search setup to return matching Record objects based on criteria. It builds an Expression Tree based on the type of criteria and queries the database to return matches. Previously, it only matched on top-level properties, but now I'm trying to search within child properties of Record and build an Expression that may be combined with others in a query. The objects look like this:

public class Record
{
    public virtual ICollection<Attribute> Attributes { get; set; }
}

public class Attribute
{
    public Guid AttributeId { get; set; }
    public string Value { get; set; }

    public virtual Record Record { get; set; }
}

One of the possible criteria is a value pair of AttributeId and Value, for which I would return all Record rows which have an Attribute matching both values. For example, if I was just writing a simple LINQ query it would look like this:

Guid searchId = Guid.NewGuid();
string searchValue = "asdf";

IQueryable<Record> records = GetSomeQueryToRecords();
IEnumerable<Record> recordsThatMatch = records
    .Where(r => r.Attribute.Any(a => a.AttributeId == searchId && a.Value == searchValue));

I need to instead build this into my Expression tree so it will work both in-memory and for EF Core queries. The Expression returned by GetExpression would end up being combined with other Expressions using AndAlso to build a large query down the line. I have tried this:

ParameterExpression record = Expression.Parameter(typeof(Record));
Expression searchExpression = GetExpression(record, searchId, searchValue);

Expression<Func<Record, bool>> lambda = Expression.Lamda<Func<Record, bool>>(searchExpression, record);
recordsThatMatch = records.Where(lambda);


private Expression GetExpression(ParameterExpression parameter, Guid searchId, string searchValue)
{
    // Get the Record.Attributes Memboer.
    MemberExpression attributes = Expression.Property(parameter, nameof(Record.Attributes));

    // Get the "record.Attributes.Any(predicate)" Method.
    MethodInfo anyMethod = typeof(Enumerable).GetMethods()
        .Where(m => m.Name == nameof(Enumerable.Any))
        .Where(m => m.GetParameters().Length == 2)
        .Single();
    
    // Make Method generic.
    anyMethod = anyMethod.MakeGenericMethod(typeof(Attribute));

    ParameterExpression attribute = Expression.Parameter(typeof(Attribute));

    Expression idEqualExpression = Expression.Equal(
        Expression.Property(attribute, nameof(Attribute.AttributeId)),
        Expression.Constant(searchId)
    );

    Expression valueEqualExpression = Expression.Equal(
        Expression.Property(attribute, nameof(Attribute.Value)),
        Expression.Constant(searchValue)
    );

    Expression attributeMatchExpression = Expression.AndAlso(idEqualExpression, valueEqualExpression);

    return Expression.Call(null, anyMethod, attributes, attributeMatchExpression);
}

I get an ArgumentException with Expression.Call though, because honestly I don't know how to use it for this particular instance and it has many overrides. I believe I should be passing null since I actually want a static method as .Any() is an extension method.

CodePudding user response:

You have forgot to create 'LambdaExpresson'. Also Expression.Call can besimplified.

private Expression GetExpression(ParameterExpression parameter, Guid searchId, string searchValue)
{
    // Get the Record.Attributes Memboer.
    MemberExpression attributes = Expression.Property(parameter, nameof(Record.Attributes));

    ParameterExpression attribute = Expression.Parameter(typeof(Attribute));

    Expression idEqualExpression = Expression.Equal(
        Expression.Property(attribute, nameof(Attribute.AttributeId)),
        Expression.Constant(searchId)
    );

    Expression valueEqualExpression = Expression.Equal(
        Expression.Property(attribute, nameof(Attribute.Value)),
        Expression.Constant(searchValue)
    );

    Expression attributeMatchExpression = Expression.AndAlso(idEqualExpression, valueEqualExpression);

    var matchLambda = Expression.Lambda(attributeMatchExpression, attribute);

    return Expression.Call(typeof(Enumerable), nameof(Enumerable.Any), new [] {typeof(Attribute)}, attributes, matchLambda);
}
  • Related