Skip to content

Unable to use extension everything to write a generic Cast that works with LINQ query comprehensionsΒ #78415

Closed

Description

@stephentoub

The LINQ specification expands:

from T t in source

to use source.Cast<T>().

For Enumerable.Cast<T>, this works because the source is a non-generic IEnumerable, and thus there's only the one generic type parameter for the result.

For AsyncEnumerable.Cast, this falls down. Such a Cast extension method today needs to be generic on both the TSource and TResult, e.g.

public static IAsyncEnumerable<TResult> Cast<TSource, TResult>(this IAsyncEnumerable<TSource> source)

and the C# compiler refuses to bind the from cast syntax when the arity of Cast doesn't match.

I was expecting extension everything would fix this, e.g.

extension<TSource>(IAsyncEnumerable<TSource> source)
{
    public IAsyncEnumerable<TResult> Cast<TResult>() { ... }
}

but unfortunately it doesn't. While I can write the above, it doesn't provide any expressive power that didn't already exist with extension methods today, because the resulting method still has a generic arity of 2, and I still need to provide both type arguments in order to use it, e.g.

IAsyncEnumerable<Base> source = ...;
var result = source.Cast<Base, Derived>();

and the C# compiler refuses to use it to satisfy Cast.

IAsyncEnumerable<Derived> results1 = from x in source // OK
                                     select (Derived)x;

IAsyncEnumerable<Derived> results2 = from Derived x in source // Error!!
                                     select x;

cc: @jcouv, @MadsTorgersen

Activity

  1. ghost added
    untriagedIssues and PRs which have not yet been triaged by a lead
    on May 2, 2025
  2. self-assigned this
    on May 2, 2025
  3. changed the title [-]Unable to use extension everything to write a generic Cast that works with LINQ[/-] [+]Unable to use extension everything to write a generic Cast that works with LINQ query comprehensions[/+] on May 2, 2025
  4. julealgon commented on May 2, 2025

    @julealgon

    @stephentoub would breaking the method down into 2 components help here?

    public static AsyncEnumerableCaster<TResult> Cast<TSource>(this IAsyncEnumerable<TSource> source)
    public class AsyncEnumerableCaster
    {
        public IAsyncEnumerable<TResult> To<TResult>();
    }

    Obviously it's not ideal since the call pattern then becomes different than the original IEnumerable one.

    I've used this trick many times to overcome the lack of partial generic type inference in C#. A common example for us:

    public TTarget Map<TSource, TTarget>(TSource source);

    Becomes:

    public Mapping<TSource> Map<TSource>(TSource source);
    public class Mapping<TSource>
    {
        TTarget To<TTarget>();
    }
  5. stephentoub commented on May 2, 2025

    @stephentoub
    MemberAuthor

    How does that help the LINQ query comprehension?

  6. julealgon commented on May 2, 2025

    @julealgon

    How does that help the LINQ query comprehension?

    Well it was more of a question as to whether it could help or not. If the issue was the lack of partial inference, and an additional mapping could be added for Cast().To<T>() instead of just Cast<T>() in the pattern recognized by from T t in source that could work.

    Otherwise just ignore the idea 😝

  7. stephentoub commented on May 2, 2025

    @stephentoub
    MemberAuthor

    Thanks, but if the pattern for from T t in source is going to be modified, it could also be modified to recognize the two-generic-parameter signature.

  8. aromaa commented on May 6, 2025

    @aromaa

    Maybe introducing discards for generic arguments could solve this?

    extension(IEnumerable<_>)
    {
        // 1-phase lookup, for compat, generic argument in the method declaration
        public static IEnumerable<T> M<T>(out T t) => ...;
    }
    
    extension<T>(IEnumerable<T>)
    {
        // 2-phase lookup, generic argument in the extension declaration
        public static IEnumerable<T> M(out T t) => ...;
    }
  9. Blaisor commented on May 24, 2025

    @Blaisor

    I don't see TSource in the official implementation: AsyncEnumerable.Cast(IAsyncEnumerable)
    Wouldn't this extension method work for you?

  10. stephentoub commented on May 24, 2025

    @stephentoub
    MemberAuthor

    I don't see TSource in the official implementation: AsyncEnumerable.Cast(IAsyncEnumerable) Wouldn't this extension method work for you?

    I wrote that extension, as a compromise. That shape doesn't work with sources that are of value types, unconstrained generics, etc.

  11. Blaisor commented on May 24, 2025

    @Blaisor

    I wrote that extension, as a compromise. That shape doesn't work with sources that are of value types, unconstrained generics, etc.

    I see. Please make your concerns heard in dotnet/csharplang#8696 where my alternate proposal would actually address this issue.

  12. martin-steinhoefer commented on May 26, 2025

    @martin-steinhoefer

    I don't see TSource in the official implementation: AsyncEnumerable.Cast(IAsyncEnumerable) Wouldn't this extension method work for you?

    I wrote that extension, as a compromise. That shape doesn't work with sources that are of value types, unconstrained generics, etc.

    But doesn't suffer Enumerable.Cast<T> from a very similar problem?

  13. stephentoub commented on May 26, 2025

    @stephentoub
    MemberAuthor

    But doesn't suffer Enumerable.Cast from a very similar problem?

    No, it operates over the non-generic IEnumerable, and code like:
    https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA+ABADAAgwRhQG4BYAKGz3wDoAZASwDsBHUs8ggZgB4mAXAHw4YOALw4A2vgA0OAEyzOAXTbkAbgEMoOWAGcArgBs+YnADMoEALY5+t242HkcL129e6YhmGBP0iQA===
    works fine.

  14. martin-steinhoefer commented on May 26, 2025

    @martin-steinhoefer

    But what, if one writes:

    IEnumerable<int> e = [1, 2, 3];
    
    var result = from long i in e
                 select i;

    That would not work - as with any value type - because of the boxing performed under the hood...

    And isn't the idea of having a Cast<T> operator exactly that - converting the element to the desired type (note, that (long)i is possible). With reference types one can cast to arbitrary target types (as long as there is a conversion).

    BTW: how would a fully generic version of Cast<T,TResult> look like - I seem to miss something with the generics...

  15. added theissue type on May 27, 2025
  16. added this to the Next milestone on May 27, 2025
  17. jcouv commented on May 28, 2025

    @jcouv
    Contributor

    From offline and LDM discussion, the is by-design. The extensions everything feature does not make this better or worse. We would likely need a proposal for LINQ to solve this. I'll move to csharplang

  18. Blaisor commented on May 28, 2025

    @Blaisor

    From offline and LDM discussion, the is by-design. The extensions everything feature does not make this better or worse. We would likely need a proposal for LINQ to solve this. I'll move to csharplang

    Please consider adding support for generic containing classes for extensions.

  19. locked and limited conversation to collaborators on May 28, 2025
  20. converted this issue into a discussion #78730 on May 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions