Reputation: 2139
I have this LINQ Expression that finds all the historical changes to a given customer's CreditBalance:
var history = GetHistory(id);
var changes = history.Where(x => history.Where(y => y.AuditId < x.AuditId)
.OrderByDescending(y => y.AuditId)
.Select(y => y.CreditBalance)
.FirstOrDefault() != x.CreditBalance);
This function works as expected. What I want to do is change this function to allow the user to query changes to any historical field. The way I chose to tackle this was with expression trees.
So far I have come up with this solution:
var history = GetHistory(id);
var c = Expression.Parameter(typeof(Customer_history), "c");
var d = Expression.Parameter(typeof(Customer_history), "d");
var cAudit = Expression.Property(c, typeof(Customer_history).GetProperty("AuditId"));
var dAudit = Expression.Property(d, typeof(Customer_history).GetProperty("AuditId"));
var whereBody = Expression.LessThan(dAudit, cAudit);
var whereLambda = Expression.Lambda(whereBody, d);
var where = Methods.QueryableWhere.MakeGenericMethod(typeof(Customer_history));
var whereCall = Expression.Call(null, where, **Expression.Constant(history)**, whereLambda);
var orderByLambda = Expression.Lambda(dAudit, d);
var orderBy = Methods.QueryableOrderByDescending.MakeGenericMethod(typeof(Customer_history), orderByLambda.Body.Type);
var orderByCall = Expression.Call(null, orderBy, whereCall, orderByLambda);
var dProp = Expression.Property(d, typeof(Customer_history).GetProperty(field));
var selectLambda = Expression.Lambda(dProp, d);
var select = Methods.QueryableSelect.MakeGenericMethod(typeof(Customer_history), selectLambda.Body.Type);
var selectCall = Expression.Call(null, select, orderByCall, selectLambda);
var firstOrDefault = Methods.QueryableFirstOrDefault.MakeGenericMethod(selectLambda.Body.Type);
var firstOrDefaultCall = Expression.Call(null, firstOrDefault, selectCall);
var cProp = Expression.Property(c, typeof(Customer_history).GetProperty(field));
var comparison = Expression.NotEqual(firstOrDefaultCall, cProp);
var lambda = Expression.Lambda<Func<Customer_history, bool>>(comparison, c);
var changes = history.Where(lambda);
The problem is, I get this exception when the query is executed:
Unable to create constant value of type 'Namespace.Customer_history'. Only primitive types or enumeration types are supported in this context.
Now I am assuming that the issue is the Expression.Constant(history) statement based on the exception message. The problem is, I don't know how to rewrite it to allow the query provider to handle it appropriately. I know it works because of the original query, I just don't know how to do it in an expression tree.
Can anyone provide any direction?
Upvotes: 1
Views: 751
Reputation: 11319
You can always try using dynamic linq, which allows you to use strings as expressions instead of lambda.
Example:
var query = history.Where("MyField = MyFilter");
https://www.nuget.org/packages/System.Linq.Dynamic.Library/
https://github.com/NArnott/System.Linq.Dynamic
Upvotes: 0
Reputation: 2139
As suspected, it seems that a ConstantExpression is not the way to obtain the value from a local variable.
What I needed to do was create a private class to store the variable in, and then I was able to access it with a field MemberExpression
private class ValueHolder<T>
{
public IQueryable<T> History;
}
Then in my method I was able to have the expression evaluated using this:
var valueHolder = new ValueHolder<T>
{
History = data
};
var c = Expression.Parameter(typeof(T), "c");
var constantEx = Expression.Constant(valueHolder);
var fieldEx = Expression.Field(constantEx, valueHolder.GetType().GetField("History"));
Upvotes: 1