Question

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?

Was it helpful?

Solution

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

OTHER TIPS

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

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top