Usually - and since a while - this solved using immutable collections.
Your public properties should be, for example, of type IImmutableList<T>
, IImmutableHashSet<T>
and so on.
Any IEnumerable<T>
can be converted to an immutable collection:
someEnumerable.ToImmutableList();
someEnumerable.ToImmutableHashSet();
- ... and so on.
This way you can work with private properties using mutable collections and provide a public surface of immutable collections only.
For example:
public class A
{
private List<string> StringListInternal { get; set; } = new List<string>();
public IImmutableList<string> StringList => StringListInternal.ToImmutableList();
}
There's also an alternate approach using interfaces:
public interface IReadOnlyA
{
IImmutableList<string> StringList { get; }
}
public class A : IReadOnlyA
{
public List<string> StringList { get; set; } = new List<string>();
IImmutableList<string> IReadOnlyA.StringList => StringList.ToImmutableList();
}
Check that IReadOnlyA
has been explicitly-implemented, thus both mutable and immutable StringList
properties can co-exist as part of the same class.
When you want to expose an immutable A
, then you return your A
objects upcasted to IReadOnlyA
and upper layers won't be able to mutate the whole StringList
in the sample above:
public IReadOnlyA DoStuff()
{
return new A();
}
IReadOnlyA a = DoStuff();
// OK! IReadOnly.StringList is IImmutableList<string>
IImmutableList<string> stringList = a.StringList;
Avoiding converting the mutable list to immutable list every time
It should be a possible solution to avoid converting the source list into immutable list each time immutable one is accessed.
Equatable members
If type of items overrides Object.Equals
and GetHashCode
, and optionally implements IEquatable<T>
, then both public immutable list property access may look as follows:
public class A : IReadOnlyA
{
private IImmutableList<string> _immutableStringList;
public List<string> StringList { get; set; } = new List<string>();
IImmutableList<string> IReadOnlyA.StringList
{
get
{
// An intersection will verify that the entire immutable list
// contains the exact same elements and count of mutable list
if(_immutableStringList.Intersect(StringList).Count == StringList.Count)
return _immutableStringList;
else
{
// the intersection demonstrated that mutable and
// immutable list have different counts, thus, a new
// immutable list must be created again
_immutableStringList = StringList.ToImmutableList();
return _immutableStringList;
}
}
}
}