How to create and use composite lists in C#

There’s really no such thing as a “composite” list. Or well, not in the language as a native concept anyway. Nevertheless, I was put into a spot at work where I needed a way to have these sort of “parent-child” lists. Lets clarify what I mean by this:

Consider, a list of persons. Now, say you need to filter this list out to seggregate by gender. An Enumerable.Where can easily take care of that. Except the two filtered lists you get have no relation to the original list. And so if you decide to add a female member to the female member list, it won’t add it to the original list. Which means, running the filter again would not get you any new members, just those that were in the original list.

A lot of the times if you’re needing to do this, you need a better structure in place. But, something brought you here and so this is how you implement a composite list where additions and removals are propagated up to the parent list.

Consider a simple Person class:

class Person
{
    public int Id { get; set; }
    public string Name { get; set; } = "Unknown";
    public int Age { get; set; }
    public Sex Sex { get; set; }

    public Person(int id, string name, int age, Sex sex)
    {
        Id = id;
        Name = name;
        Age = age;
        Sex = sex;
    }

    public override string ToString() => $"Name: {Name}, Age: {Age}, Sex: {Sex}";
}

And a collection Persons class:

internal class Persons : List<Person>
{
    public override string ToString()
    {
        var result = string.Empty;
        foreach (var person in this)
        {
            result += (person + "\n\r");
        }
        return result;
    }
}

A Community class that uses this collection:

internal class Community
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public Persons Members { get; set; } = new Persons();
    public SegeratedPersons MaleMembers 
    {
        get => GetFilteredBySex(Sex.Male);
    }
    public SegeratedPersons FemaleMembers
    {
        get => GetFilteredBySex(Sex.Female);
    }

    private SegeratedPersons GetFilteredBySex(Sex sex)
    {
        var persons = SegeratedPersons.GetInstance(Members, sex);
        return persons.GetFiltered();
    }
}

Now, all we need is a class that manages the children lists. The segregated lists need to inherit the parent’s list and create a derived class from there that manages the modifications to the list. In SegeratedPersons, if you try to add a male to the female filtered class: it’ll throw an exception and vice versa. However, if you add the matching type then it’ll also add it to the parent so everytime you run the filter, it actually works as expected.

internal class SegeratedPersons : Persons
{
    private static SegeratedPersons? _instance = null;
    private static readonly object Padlock = new object();
    private static Persons _baseList = new();
    private static Sex _sex;

    private SegeratedPersons(Persons persons, Sex sex)
    {
        _baseList = persons;
        _sex = sex;
    }

    public static SegeratedPersons GetInstance(Persons persons, Sex sex)
    {
        // Thread-safe singleton
        lock (Padlock)
        {
            _instance ??= new SegeratedPersons(persons, sex);
            _sex = sex;
            _baseList = persons;
            return _instance;
        }
    }

    public new void Add(Person person)
    {
        if (person == null)
            return;
        if (!IsCompatible(person))
            ThrowIncompatibleException();
        else
            _baseList.Add(person);
    }

    public new Person this[int index]
    {
        get => _baseList[index];
        set
        {
            var selected = base[index];
            int indexInBase = _baseList.IndexOf(selected);
            _baseList[indexInBase] = value;
        }
    }

    public new void Clear()
    {
        var itemsToRemove = _baseList.Where(IsCompatible);
        itemsToRemove.ToList().ForEach(Remove);
    }

    public SegeratedPersons GetFiltered()
    {
        base.Clear();
        AddPersons(_sex);
        return this;
    }

    private void AddPersons(Sex sex)
    {
        foreach (var person in _baseList.Where(person =>
                     person.Sex == sex))
            base.Add(person);
    }

    internal new void Remove(Person person) => _baseList.Remove(person);

    private void ThrowIncompatibleException() => throw new InvalidOperationException($"Trying to add incompatible {_sex} to list");

    private bool IsCompatible(Person person) => person.Sex == _sex;
}

You can find the complete executable console app demonstrating this functionality here: https://github.com/dydx-git/CompositeLists