Implementing a defaultable collection in C#

This quick blog post shows one solution to a problem that you might have when binding to configuration (appsettings.json or similar) in your .NET app.

The problem: #

I want to bind a collection from config, but if the config doesn't exist, I want to use some default values. I want either what's in config, or the default values, but not both

For instance, given this type:

public class MyImageProcessingOptions
{
public IList<int> ResizeWidths { get; } = new List<int> { 100, 200, 400, 800 };
}

If the config doesn't contain an entry for ResizeWidths, then the values should be the defaults. But if config does contain that entry, e.g in the appsettings.json file:

{
"MyImageProcessingOptions": {
"ResizeWidths": [
"42",
"69",
"666"
]
}
}

... then those values should be used and the default values should disappear.

The current behaviour when binding collections is to append any values in config to the collection you created in your code.

At the time of writing, there is an open API Proposal for a new flag in BinderOptions to overwrite the collection. But there's an easier way...

The solution #

(or one of them)

Without requiring any Runtime API changes, one solution is to hand-role your own defaultable collection:

public class DefaultableCollection<T> : ICollection<T>
{
private readonly List<T> _items;
private bool _overridden;

public DefaultableCollection(params T[] defaults) => _items = defaults.ToList();

public void Add(T value)
{
if (!_overridden)
{
_overridden = true;
_items.Clear();
}

_items.Add(value);
}

public void Clear() => _items.Clear();

public bool Contains(T item) => _items.Contains(item);

public void CopyTo(T[] array, int arrayIndex) => _items.CopyTo(array, arrayIndex);

public bool Remove(T item) => _items.Remove(item);

public int Count => _items.Count;

public bool IsReadOnly => false;

public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

... and then just declare your collection to be a DefaultableCollection instead of IList:

public class MyImageProcessingOptions
{
public DefaultableCollection<int> ResizeWidths { get; } = new(100, 200, 400, 800);
}

What have we seen? #

We've seen a situation where you might want either the default values from a collection, or the values from configuration, but not both. And we've seen that deriving your own type from ICollection<T> allows us to do that.

The code above is in this github repo

🙏🙏🙏

Since you've made it this far, sharing this article on your favorite social media network would be highly appreciated 💖! For feedback, please ping me on Twitter.

Leave a comment

Comments are moderated, so there may be a short delays before you see it.

Published