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

      

+3


source to share


3 answers


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.

0


source


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 from IQueryable

    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 delegate Func<Airplane, bool>

. Loop over each item in the list, calling it a delegate. If the delegate returns true

, then yield

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 statement Where

. Then add that as a sentence Where

to the earlier expression (which translates to SELECT * 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.
0


source


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.

-1


source







All Articles