Why can't I use a range value property as a parameter when calling a method in a LINQ query?
Where is my test function:
public bool Test(string a, string b)
{
return a.Contains(b);
}
Why it works:
var airplanes = _dataContext.Airplanes.Where(p => Test("abc", "a"));
And this work:
string s = "abc";
var airplanes = _dataContext.Airplanes.Where(p => Test(s, "a"));
And this works:
var airplanes = _dataContext.Airplanes.Where(p => Test(new Random().Next(1, 10).ToString(), "1"));
And this work:
var airplanes = _dataContext.Airplanes.Where(p => p.Status.Contains("a"));
But this doesn't work:
var airplanes = _dataContext.Airplanes.Where(p => Test(p.Status.ToString(), "a");
And throws an error instead:
The first exception of type 'System.NotSupportedException' occurred in System.Data.Linq.dll
Additional information: Method Boolean Test (System.String, System.String) 'does not support translation to SQL.
Initially, I was passing the whole variable p
to the function and I thought maybe the problem was that this parameter was a normal, not recognized SQL class, so I made the parameters just string properties of the class, but didn't do it, Solve something.
Why can't I use a range variable property as a parameter? Is there a way to get around this? Because it means that instead of incredibly ugly linq queries, I can break it down into nice little methods.
Edit: Also, why does this example work when it seems like it does the same thing, passing a property to the iterated variable as a parameter:
private bool IsInRange(DateTime dateTime, decimal max, decimal min)
{
decimal totalMinutes = Math.Round((dateTime - DateTime.Now).TotalMinutes, 0);
return totalMinutes <= max && totalMinutes > min;
}
// elsewhere
.Where(m => IsInRange(m.DateAndTime, 30, 0));
source to share
It turned out that in the example I was looking at, the method Where
did not run on the database itself, but the list was already pulled from the database.
so this doesn't work:
var airplanes = _dataContext.Airplanes.Where(p => Test(p.Status.ToString(), "a");
but this does:
var airplanes = _dataContext.Airplanes.ToList().Where(p => Test(p.Status.ToString(), "a");
Although in my case this kind of defeat is the purpose of doing LINQ instead of iterating.
source to share
In the first two cases, the call to Test is not a parameter in the lambda, so they both decrease to p => true
.
In the third case, it happens in a similar way, although sometimes it decreases to p => true
and p => false
, but anyway, when it comes to creating an expression, the result of the call is Test
found and then fed into the expression as a constant.
The fourth expression contains subexpressions that access the property of the object and the caller Contains
, both of which EF understands and can be converted to SQL.
The fifth expression contains subexpressions that access the property and call Test
. EF doesn't understand how to translate the call to Test
, so you either need to link it to a SQL function or rewrite Test
it to create an expression rather than evaluating the result directly.
More on expressions as promised:
Let's start with two things that you may already know all about, but if you don't, then everything else will be harder to understand.
First, it means p => p.Status.Contains("a")
.
Which in itself is absolutely nothing. Unlike most expressions in C #, lambdas cannot have a type without context.
1 + 3
has a type, which int
and therefore var x = 1 + 3
gives the x
type to the compiler int
. Even long x = 1 + 3
starts with an expression int
1 + 3
and then passes it on after that long
.
p => p.Status.Contains("a")
has no type. (Airplane p) => p.Status.Contains("a")
Doesn't even have a type, so it's var λ = (Airplane p) => p.Status.Contains("a");
not allowed.
Instead, the type of the lambda expression will be either a delegate type or Expression
that is strongly typed for the delegate. So all of this is allowed (and means something):
Func<Airplane, bool> funcλ = p => p.Status.Contains("a");
Expression<Func<Airplane, bool>> expFuncλ = p => p.Status.Contains("a");
delegate bool AirplanePredicate(Airplane plane);
AirplanePredicate delλ = p => p.Status.Contains("a");
Expression<AirplanePredicate> expDelλ = p => p.Status.Contains("a");
Good. You may have known this if not now.
Second, what does Where
Linq do.
The form Queryable
Where
(for now we will ignore the form Enumerable
and return to it) is defined as follows:
public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)
IQueryable<T>
represents something that 0 or more items can get. It can do four things using four methods:
- Give you an enumerator to enumerate these elements (inherited from
IEnumerable<T>
). - Tell you what type of element it has (which will
typeof(T)
, but inherited fromIQueryable
where it's not so obvious). - Tell us what the request provider is.
- Tell us what this expression is.
Now, those last two are the important bits here.
If you start with new List<Airplane>().AsQueryable
, the query provider will be EnumerableQuery<Airplane>
, which is the class that handles requests for in-memory enumerations Airplane
, and its expression will map to the return of that list.
If you start with _dataContext.Airplanes
, a provider will be System.Data.Entity.Internal.Linq.DbQueryProvider
, which is a class that handles EF queries about databases, and its expression will represent a run SELECT * FROM Airplanes
in the database, and then an object creation for each row is returned.
The challenge now Where
is to force the provider to create a new IQueryable
one that filters the results of the expression we start according to the one passed to it Expression<Func<Airplane, bool>>
.
The fun bit is that this is a wonderful self-promotion: the expression returned Where
when you call it with arguments IQueryable<Airplane>
and Expression<Func<Airplane, bool>>
actually represents a call Where
with arguments IQueryable<Airplane>
and Expression<Func<Airplane, bool>>
! This is how the call Where
leads to Where
: "hey, you have to call Where
here."
So what's next?
Well, sooner or later we will carry out some operation that results in IQueryable
not being used to return another IQueryable
, but another object representing the query results. For the sake of simplicity, let's say we're just starting to list the results of our only one Where
.
If these were Linq-to-objects, then we would have a query with an expression that means:
Take
Expression<Func<Airplane, bool>>
and compile it so you have a delegateFunc<Airplane, bool>
. Loop over each item in the list, calling it a delegate. If the delegate returnstrue
, thenyield
this item, otherwise not.
(This, by the way, is what the version Enumerable
Where
does directly with Func<Airplane, bool>
instead of Expression<Func<Airplane, bool>>
. Remember when I said that the result Where
was an expression that said "hey you should call Where
here"? That's pretty much what it does, but since the vendor now chooses form Enumerable
Where
and uses Func<Airplane, bool>
rather than Expression<Func<Airplane, bool>>
, we get the results we want. This also means that as long as the operations specified in IQueryable<T>
have the equivalent provided on IEnumerable<T>
linq-to-objects can serve anything that linq normally does).
But it's not linq-to-objects, it's EF, so we have an expression that means:
Take
Expression<Func<Airplane, bool>>
and turn it into a boolean SQL expression that can be used in an SQL statementWhere
. Then add that as a sentenceWhere
to the earlier expression (which translates toSELECT * FROM Airplanes
).
The hard bit here is "and turn it into a boolean SQL expression".
When your lambda was p => p.Status.Contains("a")
, then the SQL could be produced (depending on the SQL version) CONTAINS (Status, 'a')
or Status LIKE '%a%'
something else for another type of database. Hence the end result SELECT * FROM Airplanes WHERE Status LIKE '%a%'
or so on. EF knows how to break this expression into component expressions and how to turn .Status
into column access and how to turn string
Contains(string value)
into SQL statements where available.
When your lambda was p => Test(p.Status.ToString(), "a")
, the result was NotSupportedException
because EF doesn't know how to turn your method Test
into SQL.
Good. What meat, let's get to the pudding.
Can you elaborate on what you mean by "rewrite the test" so that it creates an expression rather than calculating the result directly. "
The problem here is that I don’t know what your ultimate goal was, like where you wanted to be flexible. So I'm going to do something that is equivalent in .Where(p => Test(p.Status.ToString(), someStringParameter))
three ways; the easy way, the pretty easy way, and the hard way, where they can be made more flexible in different ways.
The easiest way first:
public static class AirplaneQueryExtensions
{
public static IQueryable<Airplane> FilterByStatus(this IQueryable<Airplane> source, string statusMatch)
{
return source.Where(p => p.Status.Contains(statusMatch));
}
}
Here you can use _dataContext.Airplanes.FilterByStatus("a")
, and it is as if you were using your worker Where()
. Because that's exactly what he does. We haven't done much here, although of course there are more complex challenges for DRY Where()
.
Roughly equally easy:
public static Expression<Func<Airplane, bool>> StatusFilter(string sought)
{
return p => p.Status.Contains(sought);
}
This is where you can use _dataContext.Airplanes.Where(StatusFilter("a"))
and repeat it in much the same way as if you were using your worker Where()
. Again, we haven't done much here, but the possibilities are DRY if the filter was more complex.
Now for the fun version:
public static Expression<Func<Airplane, bool>> StatusFilter(string sought)
{
var param = Expression.Parameter(typeof(Airplane), "p"); // p
var property = typeof(Airplane).GetProperty("Status"); // .Status
var propExp = Expression.Property(param, property); // p.Status
var soughtExp = Expression.Constant(sought); // sought
var contains = typeof(string).GetMethod("Contains", new[]{ typeof(string) }); // .Contains(string)
var callExp = Expression.Call(propExp, contains, soughtExp); // p.Status.Contains(sought)
var lambda = Expression.Lambda<Func<Airplane, bool>>(callExp, param); // p => p.Status.Contains(sought);
return lambda;
}
This is pretty much the same as the previous version StatusFilter
does behind the scenes, except that it identifies types, methods, and properties using .NET metadata tokens and we use typeof()
names as well.
As the comments on each line indicate, the first line receives an expression representing the property. We don't need to provide a name because we won't be using it directly in the source code, but we call it anyway "p"
.
The next line gets PropertyInfo
for Status
, and then creates an expression that represents getting for p
, therefore p.Status
.
The next line creates an expression that represents a constant value sought
. While sought
not a constant in general, it is expressed in a generic expression we create (which is why EF was able to treat it Test("abc", "a")
as a constant true
rather than translate it).
The next line gets MethodInfo
for Contains
, and on the next line we create an expression that represents the call to result p.Status
with sought
as an argument.
Finally, we create an expression that ties them all together into an equivalent p => p.Status.Contains(sought)
and returns it.
There is clearly a lot more to it than just doing p => p.Status.Contains(sought)
. And well, that's the point of having lambdas for C # expressions, so we usually don't have to do this job.
Indeed, to have an Test
expression-based equivalent of yours , we do:
public static MethodCallExpression Test(Expression a, string b)
{
return Expression.Call(a, typeof(string).GetMethod("Contains", new[]{ typeof(string) }), Expression.Constant(b));
}
But then, to use it, we need to do more work based on expressions, because we cannot just p => Test(p.Status, "a")
because it is p.Status
not an expression in this context. We must do:
public static Expression<Func<Airplane, bool>> UseTest(string b)
{
var param = Expression.Parameter(typeof(Airplane));
return Expression.Lambda<Func<Airplane, bool>>(Test(Expression.Property(param, typeof(Airplane).GetProperty("Status")), b), param);
}
And now we can finally use _dataContext.Airplanes.UseTest("a")
. Phew!
The expression-based approach has two advantages.
- We can use it if we want to do some manipulation of expressions outside of the desired lambdas that allow for directing to, for example, fooobar.com/questions/2246606 / ... where they use along with reflection to be able to specify a property for access as a string.
- You hopefully know well enough how linq works behind the scenes to know everything you need to know to fully understand why some of the queries in your question work and some don't.
source to share
LINQ to Entities needs to know how to translate your code into a SQL query that it can run against the database.
When you use (I removed the call ToString
from the code you posted because it ToString
doesn't work with LINQ to Entities):
var airplanes = _dataContext.Airplanes.Where(p => p.Status.Contains("a"));
compiler translates your lambda into an Expression Tree that the LINQ provider can traverse and generate correct SQL:
You can force the C # or Visual Basic compiler to create an expression tree for you based on an anonymous lambda expression , or you can create expression trees manually using a namespace
System.Linq.Expressions
.a quote from Expression Trees (C # and Visual Basic)
He knows how to translate the call to IEnumerable.Contains
because this is one of the methods that was added to him as "known" stuff - he knows he has to generate a statement IN
just as he knows he !=
needs to be translated <>
to SQL.
When you use
var airplanes = _dataContext.Airplanes.Where(p => Test(p.Active, "a");
the whole expression tree has Test
method calls that the LINQ provider knows nothing about. It also cannot check its contents to find that it is actually just wrapping the call Contains
, because the methods are compiled to IL, not the Expression Tree.
application
Due to Where(p => Test("abc", "a"))
word and Where(p => Test(s, "a"))
not I'm not 100% sure, but I'm guessing the LINQ provider is smart enough to see that you are calling it with two constant values, so just trying to execute it and see if it can return a value that could be considered a constant in an SQL query.
source to share