Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[API Proposal]: Create Generalized Pattern for Collection Builder Types #112990

Open
Foxtrek64 opened this issue Feb 27, 2025 · 9 comments
Open
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Collections untriaged New issue has not been triaged by the area owner

Comments

@Foxtrek64
Copy link
Contributor

Background and motivation

Currently, as of .NET 9, there is no generic mechanism for defining collection builder types. This results in collection builders being special cased, currently IEnumerable<T> => T[] and IList<T> => List<T>. There may be others but these are the two most common scenarios.

This mechanism allows for situations like this:

public IEnumerable<T> EnumerateFoos<T>(IEnumerable<T> foos)
{
    if (foos is null)
    {
        return []; // returns an empty T[]
    }

    // Not empty, evaluate
}

#111715 proposes adding this capability for IAsyncEnumerable<T> but there is no mechanim that can be extended in a generic manner. Instead, they must be special typed just like arrays and lists. This proposal is to add a generic mechanism that can be used to decorate any current or future type.

That way we're not just continually fixing things by adding "just one more type" for each release.

API Proposal

[AttributeUsage(AttributeTargets.Class, AllowMultiple: true)]
internal sealed class CollectionBuilderForAttribute(Type enumerableType) : Attribute
{
}

I'm not entirely settled on the API design at this point but the idea is that this attribute becomes the primary driver for defining collection builders. This allows for a general, well-known, and documented solution to adding support for collection types moving forward. This should not resolve #111715 directly, but should provide a mechanism for a separate API proposal which implements the solution provided here.

API Usage

This should be a rough approximation of the special casing that List has for IList.

[CollectionBuilderFor(typeof(IList<>))]
public sealed class List<T> : IList<T>
{
    // Body omitted
}

Alternative Designs

We could stay our current course and set up a handler in the runtime/corelib, similar to how IEnumerable is handled by T[] and IList is handled by List. This is a lot of special casing for a feature that is repeated multiple times.

In .NET 10, we gain CollectionBuilderAttribute but this would not allow support to be backported to previous TFMs.

Risks

None that I'm aware of.

@Foxtrek64 Foxtrek64 added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Feb 27, 2025
@dotnet-policy-service dotnet-policy-service bot added the untriaged New issue has not been triaged by the area owner label Feb 27, 2025
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-collections
See info in area-owners.md if you want to be subscribed.

@jnm2
Copy link
Contributor

jnm2 commented Feb 27, 2025

Wouldn't this need to be a proposal to update the language specification as well?

@jnm2
Copy link
Contributor

jnm2 commented Feb 27, 2025

How is the [CollectionBuilderFor] type discovered? Is it automatically active if the compiler sees it in the current compilation or in any of the referenced assemblies?
Also, what happens if more than one type in scope is CollectionBuilderFor a given type?

@elgonzo
Copy link

elgonzo commented Feb 27, 2025

FYI, IEnumerable<T> => T[] is not true, except when the collection expression is empty.
Try using a non-empty collection expression and see what you get...

@eiriktsarpalis
Copy link
Member

I agree that discovery/ambiguity is going to be an issue with a design such as this.

@Foxtrek64
Copy link
Contributor Author

Wouldn't this need to be a proposal to update the language specification as well?

Likely, yes.

How is the [CollectionBuilderFor] type discovered? Is it automatically active if the compiler sees it in the current compilation or in any of the referenced assemblies? Also, what happens if more than one type in scope is CollectionBuilderFor a given type?

I see two options here:

  1. Keep the attribute internal. Only let the runtime decide what gets decorated and manually test to ensure a lack of collisions. This is not very ideal but it does resolve both questions.
  2. Make the attribute public and provide a mechanism to choose the one to use. This could be as simple as providing a default and in the case of ambiguity pick the one with the most proximal scope. So a collection builder in the project would have priority over one in the same solution but another project, which would in turn have priority over one defined in a nuget package, and finally that would have priority over the default one provided by the language. That said, this still leaves an open question of how to handle a situation where multiple nuget packages provide a builder for the type.

I know there are things to discuss here, but I did want to at least try to focus discussion on this option to see if we can either eliminate it as an option or find a way to implement it that works for our requirements.

@jnm2
Copy link
Contributor

jnm2 commented Feb 27, 2025

Keep the attribute internal.

What prevents a software user from declaring the attribute in the appropriate namespace in their own project or assembly? The compiler recognizes all attributes without regard to the containing assembly name. The compiler will see such a hand-declared type. What does it do, how does it know it's hand-declared versus provided by the dotnet/runtime repo?

@Foxtrek64
Copy link
Contributor Author

The compiler recognizes all attributes without regard to the containing assembly name.

Is that so? I didn't know that.

I suppose one possible answer would be to add another attribute to decorate the enumerable-returning method. This attribute would accept a type argument that allows you to specify a specific collection builder. Not sure if we would want this to be generic or to just accept a Type as a constructor argument. I think it should be pretty easy to say "if no attribute, use the collection builder defined by .NET. If there is an attribute, use the type defined."

But at this point that's about the only idea I have.

@Clockwork-Muse
Copy link
Contributor

... Although I have issues with the original example, since if nullable reference types were enabled the compiler would require that the input collection would be non-null. Which is generally good practice anyways, since in almost all cases you should be passing around empty collections in the first place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Collections untriaged New issue has not been triaged by the area owner
Projects
None yet
Development

No branches or pull requests

5 participants