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;
             }
         }
     }
}