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