1

In C# you can initialise a collection property as such:

public class MyClass
{
    public string Name { get; set ;}
}

public class MyContainerClass
{
    public ICollection<MyClass> Collection { get; set; }
}

var myObject = new MyContainerClass
               {
                   Collection = new List<MyClass>
                                {
                                    new()
                                    {
                                       Name = "1",
                                    },
                                    new()
                                    {
                                       Name = "2",
                                    },
                                },
                            }

For a class with a pre-instantiated, readonly collection you can do something similar:

public class MyReadonlyContainerClass
{
    public ICollection<MyClass> Collection { get; } = new List<MyClass>();
}

var myObject = new MyReadonlyContainerClass
               {
                   Collection = {
                                    new()
                                    {
                                       Name = "1",
                                    },
                                    new()
                                    {
                                       Name = "2",
                                    },
                                },
                            };

What I would like to know if is there is any way to use initialisation to add the members of an existing collection to a readonly collection class. For example, it might look like:

var myCollection = Enumerable.Range(1,10).Select(i => new MyClass{ Name = i.ToString() });

var myObject = new MyReadonlyContainerClass{ Collection = { myCollection } };

The above does not compile as the Add method of List<T> takes a single T instance rather than an IEnumerable<T> but I was wondering if there is a syntactically correct way to do this. Something like the spread operator in javascript maybe?

Update for context

I think from the comments below I should have been clearer about why I would like a solution to this. I am using EF Core Power Tools to generate C# classes from an existing DB schema. The EF 7 version has removed the setter from the autogenerated navigation properties (explained here). We have a test suite which will be time consuming to refactor that has instances of initialising navigation properties in the way I explained above. If a refactor is necessary then so be it, I was just looking for a language feature I may have overlooked.

SBFrancies
  • 3,987
  • 2
  • 14
  • 37
  • 1
    The [`ReadOnlyCollection`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.objectmodel.readonlycollection-1.-ctor?view=net-7.0) constructor doesn't work for you? Are you looking for a read-only collection that accepts an `IEnumerable` in its constructor? – Matthew Watson Jan 23 '23 at 13:14
  • 1
    Use the existing AddRange method or write an AddRange Extension method? – Ralf Jan 23 '23 at 13:18
  • By the way, your `MyReadonlyContainerClass` example won't compile since `Collection` is read-only because it has no setter. If you add a setter it will compile, but then the original initialisation `ICollection Collection { get; } = new List();` will be thrown away when the setter is called to initialise `Collection` in the following code. – Matthew Watson Jan 23 '23 at 13:39
  • @MatthewWatson, C# auto-implemented readonly properties have been a thing for some time now so the code will compile. They can be assigned to as above or in the class's constructor. See this answer https://stackoverflow.com/a/46381534/8532748 from over five years ago. What version of C# are you using? – SBFrancies Jan 23 '23 at 14:33
  • @Ralf see update for context. – SBFrancies Jan 23 '23 at 14:42
  • @SBFrancies [Here's the code I was talking about in DotNetFiddle](https://dotnetfiddle.net/wgAPy1) using the latest version of the compiler. It won't compile because it's trying to assign to the `Collection` property, which doesn't have a public setter. – Matthew Watson Jan 23 '23 at 15:00
  • 1
    @MatthewWatson, sorry I understand what you mean now. There is a bug (fixing in the question) but it's not what you think. I simply left the public keyword off from the `Collection` property. https://dotnetfiddle.net/AgO7DX – SBFrancies Jan 23 '23 at 15:06
  • Ah, just a typo. – Matthew Watson Jan 23 '23 at 15:17
  • @SBFrancies There is no solution if they don't bring back the setter (at least `get; init`) - see my comment on the linked GitHub issue. But according to the fact that the issue is closed, and the whole "modern" direction of breaking changes, I won't put my cents on that. Customize T4 template as EricJ suggested there, and let it generate setter for you. – Ivan Stoev Jan 23 '23 at 15:44

2 Answers2

2

If the concrete type of the property was List<T> rather than ICollection<T> then you could define an extension method called Add that takes the collection of items and calls List's AddRange method instead:

public static class Program
{
    static void Main(string[] args)
    {
        var myCollection = Enumerable.Range(1, 10).Select(i => new MyClass { Name = i.ToString() });

        var myObject = new MyReadonlyContainerClass { Collection = { myCollection } };
        Console.ReadLine();
    }
}

public class MyReadonlyContainerClass
{
    public List<MyClass> Collection { get; } = new List<MyClass>();
}


public class MyClass
{
    public string Name { get; set; }
}

public static class ConvolutedInit {
    public static void Add<T>(this List<T> collection, IEnumerable<T> items)
    {
        collection.AddRange(items);
    }
}

However, I put this in the realm of silly tricks to play with the compiler rather than something I'd recommend putting into production code. It's just confusing enough that it's more likely to trip up future readers rather than simplify things.

There's no built-in support for such a feature since AddRange isn't part of any common interface for collections.

Damien_The_Unbeliever
  • 234,701
  • 27
  • 340
  • 448
  • thank you for the answer, unfortunately I don't think it will help in this instance as I want to avoid updating the generated code (see my update for context which should have been in the original question). – SBFrancies Jan 23 '23 at 14:43
  • 2
    The solution should work also with ICollection. The extension method then just needs to loop itself as there is no AddRange then. – Ralf Jan 23 '23 at 14:51
  • It's worth noting that this won't work with an expression lambda. You will get the error: "CS8075: An extension Add method is not supported for a collection initializer in an expression lambda." I've never gotten that compiler error before. It was a little exciting. – Evil Pigeon Aug 15 '23 at 12:38
0

Thanks to the earlier answer from @Damien_The_Unbeliever I was able to work out a solution. I added an extension method to the ICollection interface.

public static class ICollectionExtensions
{
    public static void Add<T>(this ICollection<T> collection, IEnumerable<T> rangeToAdd)
    {
        foreach (var item in rangeToAdd)
        {
            collection.Add(item);
        }
    }
}

and the following code now compiles:

var myCollection = Enumerable.Range(1,10).Select(i => new MyClass{ Name = i.ToString() });

var myObject = new MyReadonlyContainerClass{ Collection = { myCollection } };

Relatively straightforward solution in the end, thanks for the help.

SBFrancies
  • 3,987
  • 2
  • 14
  • 37