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

Add an ISet<T>.AsReadOnly() extension method #29387

Closed
vanillajonathan opened this issue Apr 26, 2019 · 28 comments · Fixed by #106037
Closed

Add an ISet<T>.AsReadOnly() extension method #29387

vanillajonathan opened this issue Apr 26, 2019 · 28 comments · Fixed by #106037
Labels
api-approved API was approved in API review, it can be implemented area-System.Collections enhancement Product code improvement that does NOT require public API changes/additions in-pr There is an active PR which will close this issue when it is merged
Milestone

Comments

@vanillajonathan
Copy link
Contributor

vanillajonathan commented Apr 26, 2019

The List<> class have a AsReadOnly method to turn the collection into a ReadOnlyCollection, but HashSet<> does not have any such method.

Rationale and usage

Allows Entity Framework Core to expose a collection navigation property as a IReadOnlyCollection when backing it with a private backing field declared as a HashSet.

[Table("Blogs")]
public class Blog
{
    private HashSet<Post> _posts;
 
    public int BlogId { get; set; }
 
    public IReadOnlyCollection<Post> Posts => _posts?.AsReadOnly();
 
    public void AddPost(Post post)
    {
        // Do some business logic here...
        _posts.Add(post);
    }
}

Proposed API

namespace System.Collections.Generic;

public class CollectionExtensions
{
      public ReadOnlyCollection<T> AsReadOnly(this IList<T> list);
      public ReadOnlyDictionary<T> AsReadOnly(this IDictionary<T> dictionary);
+     public ReadOnlySet<T> AsReadOnly(this ISet<T> set);
}
@safern
Copy link
Member

safern commented May 23, 2019

Thanks @vanillajonathan I think this makes sense. I've marked the issue as api ready for review.

@omariom
Copy link
Contributor

omariom commented Sep 10, 2019

Returning ReadOnlyCollection means AsReadOnly will have to make a full copy of the HashSet.

What about a new type ReadOnlySet than can wrap any ISet<T>?
Then we will have readonly wrappers for all "ObjectModel" collections: IList, IDictionary, ObservableCollection and ISet.

@GrabYourPitchforks
Copy link
Member

Feedback - what are you actually trying to do here?

HashSet<T> already implements IReadOnlyCollection<T>, so you can cast it directly: IReadOnlyCollection<T> collection = myHashSet;.

If you want to create a ReadOnlyCollection<T>, it's one line: ReadOnlyCollection<T> collection = new ReadOnlyCollection<T>(myHashSet.ToArray());.

@vanillajonathan
Copy link
Contributor Author

I want to create a entity in Entity Framework Core. EF Core internally treats collections as hash sets but they are commonly exposed as ICollection properties in the DbSet.

I intend to employ domain-driven design (DDD), so I do not want to expose it as a ICollection since that would allow the collection to be modified.

Since HashSet (as you pointed out) implements IReadOnlyCollection<T> I can expose it as a IReadOnlyCollection (instead of a ICollection) but since that is just exposing it as a different interface it is still possible to typecast it back to a ICollection and modify it.

@GrabYourPitchforks
Copy link
Member

In your scenario, accessing the Posts property will return a brand new collection instance every single time. I'd instead recommend using the existing List<T> and ReadOnlyCollection<T> types, especially since it seems HashSet<T> might not be a good fit for the Post type.

Example:

public class Blog
{
    private List<Post> _posts = new List<Post>();
    private ReadOnlyCollection<Post> _postsAsReadOnlyCollection = new ReadOnlyCollection<Post>(_posts);

    public IReadOnlyCollection<Post> Posts => _postsAsReadOnlyCollection;

    public void AddPost(Post post)
    {
        // business logic here
        _posts.Add(post);
    }
}

Or, if you really want to use HashSet<Post>:

public class Blog
{
    private HashSet<Post> _posts = new HashSet<Post>();
    private ReadOnlyCollection<Post> _cachedROC = null;

    public IReadOnlyCollection<Post> Posts => _cachedROC ?? (_cachedROC = new ReadOnlyCollection<T>(_posts.ToArray()));

    public void AddPost(Post post)
    {
        // business logic
        _posts.Add(post);
        _cachedROC = null; // clear cached value if it exists
    }
}

@vanillajonathan
Copy link
Contributor Author

I was under the impression that ToList() would create a brand new collection instance every single time, and that AsReadOnly would not do so.

In the first of your examples I would need two private and one public instead of just one public or one private setter. Also it uses List while EF Core internally uses HashSet so the performance may differ.

In the second of your examples, it introduces caching into the business entity.

Both of these are undesirable. Could HashSet have a AsReadOnly method, and would that solve this in a clean, efficient manner?

@GrabYourPitchforks
Copy link
Member

I was under the impression that ToList() would create a brand new collection instance every single time, and that AsReadOnly would not do so.

Yes, it must create a brand new collection every time. The reason for this is that ReadOnlyCollection<T> requires the underlying collection to implement IList<T>, which HashSet<T> does not do on its own. This means that a AsReadOnly() method which returns a ReadOnlyCollection<T> necessarily needs to make a copy of the contents of the HashSet<T> instance.

@vanillajonathan
Copy link
Contributor Author

The reason for this is that ReadOnlyCollection<T> requires the underlying collection to implement IList<T>, which HashSet<T> does not do on its own.

This raises the question, should HashSet<T> implement IList<T>?

@GrabYourPitchforks
Copy link
Member

This raises the question, should HashSet implement IList?

No. HashSet<T> is an unordered no-duplicate collection. IList<T> is an ordered list, potentially with duplicates. They represent two different collection concepts.

@vanillajonathan
Copy link
Contributor Author

This raises the question, should HashSet implement IList?

No. HashSet<T> is an unordered no-duplicate collection. IList<T> is an ordered list, potentially with duplicates. They represent two different collection concepts.

Good point. Could HashSet<T> have a AsReadOnly method (like List<T> does, but without implementing IList<T>)?

With a List<T> you can cleanly do things like:

public class Blog
{
    private List<Post> _posts;
    public IReadOnlyCollection<Post> Posts => _posts?.AsReadOnly();
}

Which you cannot do as cleanly when using a HashSet<T> instead of a List<T>.

@GrabYourPitchforks
Copy link
Member

Which brings us back to the original response on the thread... given your scenario it really would just be easier to have a property that directly casts the HashSet<T> to an IReadOnlyCollection<T>.

Since HashSet (as you pointed out) implements IReadOnlyCollection I can expose it as a IReadOnlyCollection (instead of a ICollection) but since that is just exposing it as a different interface it is still possible to typecast it back to a ICollection and modify it.

So don't cast it back? :)

@vanillajonathan
Copy link
Contributor Author

Which brings us back to the original response on the thread... given your scenario it really would just be easier to have a property that directly casts the HashSet<T> to an IReadOnlyCollection<T>.

Since HashSet (as you pointed out) implements IReadOnlyCollection I can expose it as a IReadOnlyCollection (instead of a ICollection) but since that is just exposing it as a different interface it is still possible to typecast it back to a ICollection and modify it.

So don't cast it back? :)

I wouldn't typecast it back to a ICollection<T>, but if I exposed a IReadOnlyCollection<T> then I want that to be read-only, but someone using my library could typecast it back a ICollection<T> and modify it.
With a List<T> this is much easier to prevent since it has a AsReadOnly method.

@GrabYourPitchforks
Copy link
Member

So what is your concrete proposal then? Are you proposing a new type ReadOnlyCollectionWrapper<T> whose ctor takes an IReadOnlyCollection<T> instead of an IList<T>? Is this a scenario that is common enough and that would be burdensome enough for somebody to do themselves that such a type belongs in the Framework proper? Presumably if you need to be unblocked immediately you've already created such a wrapper.

@vanillajonathan
Copy link
Contributor Author

Well I don't know how feasible it is, but I expected the HashSet<T> to have a AsReadOnly method, so I would propose the addition of a AsReadOnly method to the HashSet<T>.

@GrabYourPitchforks
Copy link
Member

And what would be the return type of HashSet<T>.AsReadOnly()? As discussed it can't be ReadOnlyCollection<T>.

@vanillajonathan
Copy link
Contributor Author

And what would be the return type of HashSet<T>.AsReadOnly()? As discussed it can't be ReadOnlyCollection<T>.

You're right, it cannot return a ReadOnlyCollection<T> since that class implements IList<T> which HashSet<T> does not.

Maybe it is weird that ReadOnlyCollection<T> implements IList<T>, and that IList<T> implements ICollection<T>?
Maybe ReadOnlyCollection<T> should not have implemented IList<T>?

@GrabYourPitchforks
Copy link
Member

Yeah, in hindsight it may have made sense to have two types ReadOnlyList<T> and ReadOnlyCollection<T>. That predates my time on this team so I'm not familiar with the history behind why this decision was made. But regardless it's not a decision that can be undone right now. ☹

@vanillajonathan
Copy link
Contributor Author

Maybe things can be fixed for .NET 5?

This would be nice for developers using Entity Framework Core and who wish to implement domain-driven design (DDD).

@GrabYourPitchforks
Copy link
Member

That's not the type of breaking change we would take in .NET 5.

@msftgits msftgits transferred this issue from dotnet/corefx Feb 1, 2020
@msftgits msftgits added this to the 5.0 milestone Feb 1, 2020
@maryamariyan maryamariyan added the untriaged New issue has not been triaged by the area owner label Feb 23, 2020
@layomia layomia removed the untriaged New issue has not been triaged by the area owner label Jun 24, 2020
@layomia layomia modified the milestones: 5.0.0, Future Jun 24, 2020
@NN---
Copy link
Contributor

NN--- commented Aug 15, 2020

And what would be the return type of HashSet<T>.AsReadOnly()? As discussed it can't be ReadOnlyCollection<T>.

What about ReadOnlySet which can wrap any ISet ?
Like https://stackoverflow.com/a/36815316/558098

@eiriktsarpalis
Copy link
Member

Assuming we decided to add something like this, I think it should return an IReadOnlySet<T> instance. This begs the question whether we should be implementing a ReadOnlySet<T> class that wraps an ISet<T> instance.

@binki
Copy link

binki commented Dec 7, 2020

Wouldn’t it be better for this not to be part of the class but just an extension? Especially since HashSet<T> now implements IReadOnlySet<T>, this could just be like this:

public static class HashSetExtensions {
    public static IReadOnlySet<T> AsReadOnlySet(
        this IReadOnlySet<T> readOnlySet) {
        return readOnlySet;
    }
}

Since a direct cast exists, the rationale for including the above API would be the same as for having Enumerable.AsEnumerable<TSource>()—to support the use of implicit typing and/or method overload resolution, especially if T in IReadOnlySet<T> is big, impractical, or impossible to provide explicitly.

For wrapping ISet<T>, I’d expect a thin wrapper and the return type to be IReadOnlySet<T> instead of doing something like returning an instance of ReadOnlyCollection<T>. Then the reader can see updates made to the ISet<T> by others (remember, read-only and immutable are very different concepts even though immutable implies unwritable). The biggest issue with this approach would be the specificity/overloads for the appropriate extension methods. Since HashSet<T> is both ISet<T> and IReadOnlySet<T>, the following would create an ambiguity error if merged with the above code:

public static class HashSetExtensions {
    public static IReadOnlySet<T> AsReadOnlySet(
        this ISet<T> set) {
        return new ReadOnlySetAdapter<T>(set);
    }

    class ReadOnlySetAdapter<T> : IReadOnlySet<T> {}
}

@eiriktsarpalis
Copy link
Member

Closing -- would not support exposing an extension method as proposed. It would need to return IReadOnlySet<T>, but this should only be considered in conjunction with a ReadOnlySet<T> proposal à la ReadOnlyCollection<T>.

@ghost ghost locked as resolved and limited conversation to collaborators Nov 28, 2021
@stephentoub stephentoub reopened this Jun 18, 2024
@dotnet dotnet unlocked this conversation Jun 18, 2024
@stephentoub
Copy link
Member

ReadOnlySet<T> now exists in .NET 9. We could choose to have an ReadOnlySet<T> AsReadOnly() if desirable.

@eiriktsarpalis eiriktsarpalis added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-needs-work API needs work before it is approved, it is NOT ready for implementation labels Jun 18, 2024
@eiriktsarpalis
Copy link
Member

I've updated the API proposal to add an extension method in CollectionExtensions.

@eiriktsarpalis eiriktsarpalis changed the title Add HashSet.AsReadOnly Add an ISet<T>.AsReadOnly() extension method Jun 18, 2024
@terrajobst
Copy link
Member

terrajobst commented Aug 6, 2024

Video

  • Looks good as proposed
namespace System.Collections.Generic;

public partial class CollectionExtensions
{
    // Existing:
    // public ReadOnlyCollection<T> AsReadOnly(this IList<T> list);
    // public ReadOnlyDictionary<T> AsReadOnly(this IDictionary<T> dictionary);
    public ReadOnlySet<T> AsReadOnly(this ISet<T> set);
}

@terrajobst terrajobst added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Aug 6, 2024
@dotnet-policy-service dotnet-policy-service bot added the in-pr There is an active PR which will close this issue when it is merged label Aug 6, 2024
@silkfire
Copy link

Was there any reason this extension didn't make it into .NET 9?

@terrajobst
Copy link
Member

Read the discussion in the linked PR #106037

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-approved API was approved in API review, it can be implemented area-System.Collections enhancement Product code improvement that does NOT require public API changes/additions in-pr There is an active PR which will close this issue when it is merged
Projects
None yet
Development

Successfully merging a pull request may close this issue.