Matt
Matt

Reputation: 27001

Why can't I replace IEnumerable<T> by a generic type variable in extension method?

I am trying to make an extension method more generic to avoid redundancy (Here is an example of some real code, the code below is just to demonstrate the issue - I had the idea to make the method available for IQueryable<T> as well).

The following works fine:

public static class Extensions
{
    public static IEnumerable<T> MySelect1<T, V>(this IEnumerable<T> query, Func<T, V> f)
    {
        // do something, then return IEnumerable<T>
        var result=query.AsEnumerable<T>();
        return result;
    }
    public static IQueryable<T> MySelect1<T, V>(this IQueryable<T> query, Func<T, V> f)
    {
        // do something, then return IQueryable<T>
        var result = query.AsQueryable<T>();
        return result;
    }
}

I can use it in LinqPad like (when connected with the Northwind sample database):

var myQuery=(from x in Customers select x);
myQuery.AsEnumerable().MySelect1(d => d.CustomerID).Dump();
myQuery.AsQueryable().MySelect1(d => d.CustomerID).Dump();

Now I wanted to get rid of the duplicate implementation of MySelect1, so I refactored it as:

public static class Extensions
{
    public static E MySelect2<E, T, V>(this E query, Func<T, V> f)
    where E : System.Linq.IQueryable<T>, System.Collections.Generic.IEnumerable<T>
    {
        return (E)query.Select(f);
    }
}

This compiles too, but I cannot use MySelect2 the same way as I did above, consider the following:

// CS0411 The type arguments for method 'Extensions.MySelect2<E, T, V>(E, Func<T, V>)' 
// cannot be inferred from the usage. Try specifying the type arguments explicitly.
myQuery.AsEnumerable().MySelect2(d => d.CustomerID).Dump(); 
myQuery.AsQueryable().MySelect2(d => d.CustomerID).Dump();

Ok, doing what the error asks for works for this code line:

myQuery.AsQueryable()
       .MySelect2<IQueryable<Customers>, Customers, String>(d => d.CustomerID).Dump();

but not for that one:

myQuery.AsEnumerable<Customers>()
       .MySelect2<IEnumerable<Customers>, Customers, String>(d => d.CustomerID).Dump();

Here, I am getting

CS0311 The type 'System.Collections.Generic.IEnumerable<LINQPad.User.Customers>' cannot be used as type parameter 'E' in the generic type or method 'Extensions.MySelect2<E, T, V>(E, Func<T, V>)'. There is no implicit reference conversion from 'System.Collections.Generic.IEnumerable<LINQPad.User.Customers>' to 'System.Linq.IQueryable<LINQPad.User.Customers>'.

Why? And how can it be fixed? Please help.

Upvotes: 2

Views: 1298

Answers (1)

Jon Skeet
Jon Skeet

Reputation: 1500225

Why?

For exactly the reason stated in the error message: you're trying to use IEnumerable<Customers> as the type argument for E, but E has this constraint:

where E : System.Linq.IQueryable<T>

And how can it be fixed?

It can't, assuming I understand what you're trying to achieve.

There's a fundamental problem with the "simplification" you're trying to achieve: you don't actually have full duplication in your original MySelect1 methods. The first calls AsEnumerable() and the second calls AsQueryable(). You're trying to replace those with a cast, and that's just not going to work.

There's a further problem, even with your original methods: you're accepting Func<T, V> f as a parameter for your queryable-based method, which means any time you call Select or similar and passing in f, you'll be calling Enumerable.Select instead of Queryable.Select. To really use IQueryable<> properly, you should accept Expression<Func<T, V>> f instead. At that point, you won't need to call AsQueryable anyway.

Your two methods "should" take radically different paths based on whether you're using LINQ to Objects or a different LINQ provider (e.g. LINQ to SQL), and that can't be hidden as a pure implementation detail without significant changes that would probably make it less useful than you want anyway.

Upvotes: 11

Related Questions