How does method overload resolution work (LINQ Where extension method)?
Asked Answered
P

1

8

If I have a variable of type IQueryable<T> I have four extension methods for Where in namespace Systm.Linq available:

public static IQueryable<T> Where<T>(this IQueryable<T> source,
    Expression<Func<T, bool>> predicate);
public static IQueryable<T> Where<T>(this IQueryable<T> source,
    Expression<Func<T, int, bool>> predicate);
public static IEnumerable<T> Where<T>(this IEnumerable<T> source,
    Func<T, bool> predicate);
public static IEnumerable<T> Where<T>(this IEnumerable<T> source,
    Func<T, int, bool> predicate);

(The last two because IQueryable<T> inherits from IEnumerable<T>.)

If I use a variable of type ObjectQuery<T> (in namespace System.Data.Objects) I have five overloads of Where available, namely the four above (because ObjectQuery<T> implements IQueryable<T> and IEnumerable<T> among other interfaces) and in addition an instance method of this class:

public ObjectQuery<T> Where(string predicate,
    params ObjectParameter[] parameters);

If I do the same programming mistake while using either IQueryable<T> or ObjectQuery<T> I get very different compiler errors. Here is an example program (standard C# console application template in VS2010 SP1 + System.Data.Entity.dll assembly added to project references, the compiler error is in comment below the four examples):

using System.Data.Objects;
using System.Linq;

namespace OverloadTest
{
    public class Test
    {
        public int Id { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            IQueryable<Test> queryable = null;
            ObjectQuery<Test> objectQuery = null;

            var query1 = queryable.Where(t => t.Name == "XYZ");
            // no definition for "Name" in class OverloadTest.Test

            var query2 = queryable.Where(t => bla == blabla);
            // "bla", "blabla" do not exist in current context

            var query3 = objectQuery.Where(t => t.Name == "XYZ");
            // Delegate System.Func<Overload.Test,int,bool>
            // does not take 1 argument

            var query4 = objectQuery.Where(t => bla == blabla);
            // Delegate System.Func<Overload.Test,int,bool> 
            // does not take 1 argument
        }
    }
}

"Squiggles" look different as well in the compiler:

enter image description here

I understand the first two errors. But why does the compiler apparently want to use the overload number 4 (with the Func<T, int, bool> predicate) in the last two examples and doesn't tell me that "Name" isn't defined in class Test and that "bla" and "blabla" do not exist in the current context?

I had expected that the compiler can safely rule out overload number 5 (I don't pass in a string as parameter) and overload number 2 and 4 (I don't pass in a lambda expression with two parameters (t,i) => ...) but my expectation doesn't seem to be correct.

As a side note: I came across this problem when looking at this question. The questioner said there that the fourth query in the question does not compile (it has exactly the compiler error in example number 3 and 4 above), but this query is exactly the solution to his problem and to me it seems that something (a variable or property name?) is written wrong in the query (he didn't confirm this though) but this compiler error doesn't give a helpful indication what is wrong.

Edit

Refering to Martin Harris' very helpful comment below:

In example query4 the error "Delegate System.Func does not take 1 argument" is the error shown in the tooltip window when I hover over the squiggle line. In the compiler output window there are actually four errors in this order:

  • Delegate System.Func does not take 1 argument
  • "lambda expression" cannot be converted to "string" because "string" is not a delegate type
  • The name "bla" does not exist in the current context
  • The name "blabla" does not exist in the current context

But why doesn't the compiler complain with the first error for the first two examples that use IQueryable<T>?

Pueblo answered 31/7, 2012 at 10:49 Comment(8)
I can't answer the question, but interestingly LinqPad emits four compiler errors for query4 - "Delegate System.Func<Overload.Test,int,bool> does not take 1 argument", "Cannot convert lambda expression to type 'string'", "The name blah does not exist" and "The name blabla does not exist". So it would appear that VS is only outputting the first compiler error but the compiler itself is throwing errors for all the possible overloads. Why it doesn't do this for IQueryable I have no idea...Whereto
@MartinHarris: Interesting observation!Pueblo
@MartinHarris: VS actually outputs the same four errors in the output window. I did the mistake to only look at the tooltip error window when I hover over the squiggle. This window indeed only shows the first error (see my Edit above).Pueblo
If you change the 3rd query to var query3 = objectQuery.Where((t,i) => t.Name == "XYZ"); the error will be Delegate 'System.Func<OverloadTest.Program.Test,bool>' does not take 2 arguments. Seems like the compiler tries all overloads and throw out only one error message, perhaps the last method it have tried... who knows?Detrimental
EF known issues and limitationsFadeless
@Dakek, this is a compile phase issue and not a EF issue. You can generate the same messages using IQueryable variable like @Pueblo did.Detrimental
Yes, indeed, it has nothing to do with EF (therefore no EF tag under the question).Pueblo
This link msdn.microsoft.com/en-us/library/aa691336(v=vs.71).aspx talk about the rules for overload method resolution. Reading the docs one came to the conclusion that c# compiler consider the Func<Overload.Test,int,bool> as valid candidate for overload, perhaps because it cannot resolve the result type of the lambda expression. But it is not clear why it consider that method as a valid candidate for ObjectQuery but not IQueriable.Detrimental
F
4

Please read until end.

Actually it is because your code has compiler time errors.

Compiler detects correct extension method by looking at your code. In this case it is supposed to take a Test parameter and return bool parameter. Since your linq expression cannot be compiled, correct extension method cannot be detected, and compiler assumes first extension method it found is the one you wanted.

BTW, if you fix error like

var query3 = objectQuery.Where(t => t.Id == 1)

compiler will use

public static IQueryable<T> Where<T>(
       this IQueryable<T> source,
       Expression<Func<T, bool>> predicate
);

Now you should wonder why it skips method on Enumerable. It is because ObjectQuery<T> class directly implements 'IQueryable', but implements IEnumerable<T> because of IQueryable<T>.

you can see object hierarchy below
object hierarchy

Frothy answered 2/8, 2012 at 15:9 Comment(3)
"Since your linq expression cannot be compiled, correct extension method cannot be detected, and compiler assumes first extension method it found is the one you wanted." But why is the correct extension method detected with query1 and query2 (IQueryable), but not with query3 and query4 (ObjectQuery)? Only by accident?Pueblo
I believe it is no accident but a compiler bug. C# language specification - section 7.6.5.2 explains how extension methods are searched. If you look at document, then you will see that IQuerible<T> will be selected instead of IEnumerable<T> because of derivation. This prevents "Ambiguous Method" error, since same method is defined for both types. Case of ObjectQuery is should do the same but it does not.Frothy
What makes me believe this is a compiler error is a list of littler things. 1- MS did have some compiler errors in past. 2- ReSharper correctly identifies correct extension methods bot for interface and class and gives Ambiguous Method error.Frothy

© 2022 - 2024 — McMap. All rights reserved.