The following is a quote from Microsoft's framework design guidelines:
Collection Properties and Return Values
X DO NOT provide settable collection properties.
Users can replace the contents of the collection by clearing the collection first and then adding the new contents. If replacing the whole collection is a common scenario, consider providing the AddRange method on the collection.
I agree with the guideline - however I feel it combines badly with the C# initialization syntax.
public class RequestParameter
{
Collection<string> FilesToDownload { get; } = new Collection<string>();
Collection<string> FilesToRemove { get; } = new Collection<string>();
}
This prevents the API user from doing anything like:
client.SendRequest(new RequestParameter
{
FilesToDownload = { itemsToDownload.Select( item => item.path ) },
FilesToRemove = { GetFilesToRemove() },
});
There are four alternatives I've came up with:
Constructors with named parameters
The constructor would need to define all type fields as parameters as I don't want to make it a common case that the users would need to mix constructor parameters and initializer syntax.
This makes the constructors not backwards compatible if we need to add new fields to the type. This is an API on top of a JSON-based API where I want to reserve the ability to add additional fields to the types without breaking our clients.
Settable properties
Disregard the "no setter" guideline would be an "easy" way out.
Add an Add(IEnumerable<T>)
method on the collections
Pragmatically this would allow the syntax I want for the initializers. However it kind of breaks the usual expectations of Add
vs AddRange
on collection types.
Add a settable IEnumerable<T> Values
property on the collections
The initializer syntax would change to FilesToRemove = { Values = myItems }
. Again the purist in me kind of dislikes this extra property just for the sake of allowing the initializer syntax to be used to define the whole collection.
Are there any other alternatives? An ideal way would be something akin to operator=
overloading in the initializer syntax to allow Foo = bar
to invoke a custom function that could do copying instead of initializing the whole collection but as far as I know, C#/.Net framework doesn't support anything like this.
This same issue also applies to object-initializers in a way. The API I'm working with requires certain JSON fields to have a value and not be null. In the .Net API I wanted to modeled these properties as read-only properties with default value:
public ItemClass Item { get; } = new ItemClass();
This would have made it clear to the user which properties can be null and which can't but it prevented the user from setting these using convenience constructors:
new Item
{
Item1 = ItemClass.FileItem( "storage", "path" ),
Item2 = ItemClass.UrlItem( "url" ),
}